EGLSpec.kt

/*
 * Copyright 2021 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.egl

import android.hardware.HardwareBuffer
import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLContext
import android.opengl.EGLSurface
import android.os.Build
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.opengl.EGLExt
import androidx.opengl.EGLExt.Companion.EGLClientWaitResult
import androidx.opengl.EGLExt.Companion.EGLSyncAttribute
import androidx.opengl.EGLExt.Companion.EGL_CONDITION_SATISFIED_KHR
import androidx.opengl.EGLExt.Companion.EGL_FALSE
import androidx.opengl.EGLExt.Companion.EGL_FOREVER_KHR
import androidx.opengl.EGLExt.Companion.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR
import androidx.opengl.EGLExt.Companion.EGL_TIMEOUT_EXPIRED_KHR
import androidx.opengl.EGLExt.Companion.eglClientWaitSyncKHR
import androidx.opengl.EGLExt.Companion.eglDestroyImageKHR
import androidx.opengl.EGLExt.Companion.eglDestroySyncKHR
import androidx.opengl.EGLImageKHR
import androidx.opengl.EGLSyncKHR

@JvmDefaultWithCompatibility
/**
 * Interface for accessing various EGL facilities independent of EGL versions.
 * That is each EGL version implements this specification.
 *
 * EGLSpec is not thread safe and is up to the caller of these methods to guarantee thread safety.
 */
@Suppress("AcronymName")
interface EGLSpec {

    /**
     * Query for the capabilities associated with the given eglDisplay.
     * The result contains a space separated list of the capabilities.
     *
     * @param nameId identifier for the EGL string to query
     */
    fun eglQueryString(nameId: Int): String

    /**
     * Create a Pixel Buffer surface with the corresponding [EGLConfigAttributes].
     * Accepted attributes are defined as part of the OpenGL specification here:
     * https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
     *
     * If a pixel buffer surface could not be created, [EGL14.EGL_NO_SURFACE] is returned.
     *
     * @param config Specifies the EGL Frame buffer configuration that defines the frame buffer
     * resource available to the surface
     * @param configAttributes Optional list of attributes for the pixel buffer surface
     */
    fun eglCreatePBufferSurface(
        config: EGLConfig,
        configAttributes: EGLConfigAttributes?
    ): EGLSurface

    /**
     * Creates an on screen EGL window surface from the given [Surface] and returns a handle to it.
     *
     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
     *
     * @param config Specifies the EGL frame buffer configuration that defines the frame buffer
     * resource available to the surface
     * @param surface Android surface to consume rendered content
     * @param configAttributes Optional list of attributes for the specified surface
     */
    fun eglCreateWindowSurface(
        config: EGLConfig,
        surface: Surface,
        configAttributes: EGLConfigAttributes?
    ): EGLSurface

    /**
     * Destroys an EGL surface.
     *
     * If the EGL surface is not current to any thread, eglDestroySurface destroys
     * it immediately. Otherwise, surface is destroyed when it becomes not current to any thread.
     * Furthermore, resources associated with a pbuffer surface are not released until all color
     * buffers of that pbuffer bound to a texture object have been released. Deferral of
     * surface destruction would still return true as deferral does not indicate a failure condition
     *
     * @return `true` if destruction of the EGLSurface was successful, false otherwise
     */
    fun eglDestroySurface(surface: EGLSurface): Boolean

    /**
     * Binds the current context to the given draw and read surfaces.
     * The draw surface is used for all operations except for any pixel data read back or copy
     * operations which are taken from the read surface.
     *
     * The same EGLSurface may be specified for both draw and read surfaces.
     *
     * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml for more
     * information
     *
     * @param drawSurface EGLSurface to draw pixels into.
     * @param readSurface EGLSurface used for read/copy operations.
     */
    fun eglMakeCurrent(
        context: EGLContext,
        drawSurface: EGLSurface,
        readSurface: EGLSurface
    ): Boolean

    /**
     * Return the current surface used for reading or copying pixels.
     * If no context is current, [EGL14.EGL_NO_SURFACE] is returned
     */
    fun eglGetCurrentReadSurface(): EGLSurface

    /**
     * Return the current surface used for drawing pixels.
     * If no context is current, [EGL14.EGL_NO_SURFACE] is returned.
     */
    fun eglGetCurrentDrawSurface(): EGLSurface

    /**
     * Initialize the EGL implementation and return the major and minor version of the EGL
     * implementation through [EGLVersion]. If initialization fails, this returns
     * [EGLVersion.Unknown]
     */
    fun eglInitialize(): EGLVersion

    /**
     * Load a corresponding EGLConfig from the provided [EGLConfigAttributes]
     * If the EGLConfig could not be loaded, null is returned
     * @param configAttributes Desired [EGLConfigAttributes] to create an [EGLConfig]
     *
     * @return the [EGLConfig] with the provided [EGLConfigAttributes] or null if
     * an [EGLConfig] could not be created with the specified attributes
     */
    fun loadConfig(configAttributes: EGLConfigAttributes): EGLConfig?

    /**
     * Create an EGLContext with the default display. If createContext fails to create a
     * rendering context, EGL_NO_CONTEXT is returned
     *
     * @param config [EGLConfig] used to create the [EGLContext]
     */
    fun eglCreateContext(config: EGLConfig): EGLContext

    /**
     * Destroy the given EGLContext generated in [eglCreateContext]
     *
     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglDestroyContext.xhtml
     *
     * @param eglContext EGL rendering context to be destroyed
     */
    fun eglDestroyContext(eglContext: EGLContext)

    /**
     * Post EGL surface color buffer to a native window
     *
     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglSwapBuffers.xhtml
     *
     * @param surface Specifies the EGL drawing surface whose buffers are to be swapped
     *
     * @return `true` if swapping of buffers succeeds, false otherwise
     */
    fun eglSwapBuffers(surface: EGLSurface): Boolean

    /**
     * Query the EGL attributes of the provided surface
     *
     * @param surface EGLSurface to be queried
     * @param attribute EGL attribute to query on the given EGL Surface
     * @param result Int array to store the result of the query
     * @param offset Index within [result] to store the value of the queried attribute
     *
     * @return `true` if the query was completed successfully, false otherwise. If the query
     * fails, [result] is unmodified
     */
    fun eglQuerySurface(surface: EGLSurface, attribute: Int, result: IntArray, offset: Int): Boolean

    /**
     * Returns the error of the last called EGL function in the current thread. Initially,
     * the error is set to EGL_SUCCESS. When an EGL function could potentially generate several
     * different errors (for example, when passed both a bad attribute name, and a bad attribute
     * value for a legal attribute name), the implementation may choose to generate any one of the
     * applicable errors.
     *
     * See https://khronos.org/registry/EGL/sdk/docs/man/html/eglGetError.xhtml for more information
     * and error codes that could potentially be returned
     */
    fun eglGetError(): Int

    /**
     * Convenience method to obtain the corresponding error string from the
     * error code obtained from [EGLSpec.eglGetError]
     */
    fun getErrorMessage(): String = getStatusString(eglGetError())

    /**
     * Creates an EGLImage from the provided [HardwareBuffer]. This handles
     * internally creating an EGLClientBuffer and an [EGLImageKHR] from the client buffer.
     *
     * When this [EGLImageKHR] instance is no longer necessary, consumers should be sure to
     * call the corresponding method [eglDestroyImageKHR] to deallocate the resource.
     *
     * @param hardwareBuffer Backing [HardwareBuffer] for the generated EGLImage instance
     *
     * @return an [EGLImageKHR] instance representing the [EGLImageKHR] created from the
     * HardwareBuffer. Because this is created internally through EGL's eglCreateImageKR method,
     * this has the KHR suffix.
     *
     * This can return null if the EGL_ANDROID_image_native_buffer and EGL_KHR_image_base
     * extensions are not supported or if allocation of the buffer fails.
     *
     * See
     * www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_get_native_client_buffer.txt
     */
    @Suppress("AcronymName")
    @RequiresApi(Build.VERSION_CODES.O)
    fun eglCreateImageFromHardwareBuffer(hardwareBuffer: HardwareBuffer): EGLImageKHR?

    /**
     * Destroy the given [EGLImageKHR] instance. Once destroyed, the image may not be used to
     * create any additional [EGLImageKHR] target resources within any client API contexts,
     * although existing [EGLImageKHR] siblings may continue to be used. `true` is returned
     * if DestroyImageKHR succeeds, `false` indicates failure. This can return `false` if the
     * corresponding [EGLContext] is not valid.
     *
     * See: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_image_base.txt
     *
     * @param image EGLImageKHR to be destroyed
     *
     * @return `true` if the destruction of the EGLImageKHR object was successful, `false` otherwise
     */
    @Suppress("AcronymName")
    fun eglDestroyImageKHR(image: EGLImageKHR): Boolean

    /**
     * Creates a sync object of the specified type associated with the
     * specified display, and returns a handle to the new object.
     * The configuration of the returned [EGLSyncKHR] object is specified by the provided
     * attributes.
     *
     * Consumers should ensure that the EGL_KHR_fence_sync EGL extension is supported before
     * invoking this method otherwise a null EGLSyncFenceKHR object is returned.
     *
     * When the [EGLSyncKHR] instance is no longer necessary, consumers are encouraged to call
     * [eglDestroySyncKHR] to deallocate this resource.
     *
     * See: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_fence_sync.txt
     *
     * @param type Indicates the type of sync object that is returned
     * @param attributes Specifies the configuration of the sync object returned
     *
     * @return the [EGLSyncKHR] object to be used as a fence or null if this extension
     * is not supported
     */
    @Suppress("AcronymName")
    fun eglCreateSyncKHR(type: Int, attributes: EGLConfigAttributes?): EGLSyncKHR?

    /**
     * Query attributes of the provided sync object. Accepted attributes to query depend
     * on the type of sync object. If no errors are generated, this returns true and the
     * value of the queried attribute is stored in the value array at the offset position.
     * If this method returns false, the provided value array is unmodified.
     *
     * See: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_fence_sync.txt
     *
     * @param sync EGLSyncKHR object to query attributes
     * @param attribute Corresponding EGLSyncKHR attribute to query on [sync]
     * @param value Integer array used to store the result of the query
     * @param offset Index within the value array to store the result of the attribute query
     *
     * @return `true` if the attribute was queried successfully, false otherwise. Failure cases
     * include attempting to call this method on an invalid sync object, or the display provided
     * not matching the display that was used to create this sync object. Additionally if the
     * queried attribute is not supported for the sync object, false is returned.
     */
    @Suppress("AcronymName")
    fun eglGetSyncAttribKHR(
        sync: EGLSyncKHR,
        @EGLSyncAttribute attribute: Int,
        value: IntArray,
        offset: Int
    ): Boolean

    /**
     * Destroys the given sync object associated with the specified display
     *
     * Consumers should ensure that the EGL_KHR_fence_sync EGL extension is supported before
     * invoking this method otherwise a null EGLSyncFenceKHR object is returned.
     * See: https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_fence_sync.txt
     *
     * @param sync Fence object to be destroyed
     *
     * @return `true` if the [EGLSyncKHR] object was destroyed successfully `false` otherwise. This
     * can return `false` if the sync object is not a valid sync object for the provided display
     * or if the display provided in this method does not match the display used to create this
     * sync in [eglCreateSyncKHR].
     */
    @Suppress("AcronymName")
    fun eglDestroySyncKHR(sync: EGLSyncKHR): Boolean

    /**
     * Blocks the calling thread until the specified sync object is signalled or until
     * [timeoutNanos] nanoseconds have passed.
     * More than one [eglClientWaitSyncKHR] may be outstanding on the same [sync] at any given
     * time. When there are multiple threads blocked on the same [sync] and the [sync] object
     * has signalled, all such threads are released, but the order in which they are released is
     * not defined.
     *
     * If the value of [timeoutNanos] is zero, then [eglClientWaitSyncKHR] simply tests the
     * current status of sync. If the value of [timeoutNanos] is the special value
     * [EGL_FOREVER_KHR], then [eglClientWaitSyncKHR] does not time out. For all other values,
     * [timeoutNanos] is adjusted to the closest value allowed by the implementation-dependent
     * timeout accuracy, which may be substantially longer than one nanosecond.
     *
     * [eglClientWaitSyncKHR] returns one of three status values describing the reason for
     * returning. A return value of [EGL_TIMEOUT_EXPIRED_KHR] indicates that the specified
     * timeout period expired before [sync] was signalled, or if [timeoutNanos] is zero,
     * indicates that [sync] is not signaled. A return value of [EGL_CONDITION_SATISFIED_KHR]
     * indicates that [sync] was signaled before the timeout expired, which includes the case
     * when [sync] was already signaled when [eglClientWaitSyncKHR] was called. If an error
     * occurs then an error is generated and [EGL_FALSE] is returned.
     *
     * If the sync object being blocked upon will not be signaled in finite time (for example
     * by an associated fence command issued previously, but not yet flushed to the graphics
     * pipeline), then [eglClientWaitSyncKHR] may wait forever. To help prevent this behavior,
     * if the [EGL_SYNC_FLUSH_COMMANDS_BIT_KHR] is set on the flags parameter and the [sync] is
     * unsignaled when [eglClientWaitSyncKHR] is called, then the equivalent flush will be
     * performed for the current EGL context before blocking on sync. If no context is
     * current bound for the API, the [EGL_SYNC_FLUSH_COMMANDS_BIT_KHR] bit is ignored.
     *
     * @param sync EGLSyncKHR object to wait on
     * @param flags Optional flags to provide to handle flushing of pending commands
     * @param timeoutNanos Optional timeout value to wait before this method returns, measured
     * in nanoseconds. This value is always consumed as an unsigned long value so even negative
     * values will be converted to their unsigned equivalent.
     *
     * @return Result code indicating the status of the wait request. Either
     * [EGL_CONDITION_SATISFIED_KHR], if the sync did signal within the specified timeout,
     * [EGL_TIMEOUT_EXPIRED_KHR] if the sync did not signal within the specified timeout,
     * or [EGL_FALSE] if an error occurs.
     */
    @Suppress("AcronymName")
    fun eglClientWaitSyncKHR(
        sync: EGLSyncKHR,
        flags: Int,
        timeoutNanos: Long
    ): @EGLClientWaitResult Int

    companion object {

        @JvmField
        val V14 = object : EGLSpec {

            // Tuples of attribute identifiers along with their corresponding values.
            // EGL_NONE is used as a termination value similar to a null terminated string
            private val contextAttributes = intArrayOf(
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, // GLES VERSION 2
                // HWUI provides the ability to configure a context priority as well but that only
                // seems to be configured on SystemUIApplication. This might be useful for
                // front buffer rendering situations for performance.
                EGL14.EGL_NONE
            )

            override fun eglInitialize(): EGLVersion {
                // eglInitialize is destructive so create 2 separate arrays to store the major and
                // minor version
                val major = intArrayOf(1)
                val minor = intArrayOf(1)
                val initializeResult =
                    EGL14.eglInitialize(getDefaultDisplay(), major, 0, minor, 0)
                if (initializeResult) {
                    return EGLVersion(major[0], minor[0])
                } else {
                    throw EGLException(EGL14.eglGetError(), "Unable to initialize default display")
                }
            }

            override fun eglGetCurrentReadSurface(): EGLSurface =
                EGL14.eglGetCurrentSurface(EGL14.EGL_READ)

            override fun eglGetCurrentDrawSurface(): EGLSurface =
                EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW)

            override fun eglQueryString(nameId: Int): String =
                EGL14.eglQueryString(getDefaultDisplay(), nameId)

            override fun eglCreatePBufferSurface(
                config: EGLConfig,
                configAttributes: EGLConfigAttributes?
            ): EGLSurface =
                EGL14.eglCreatePbufferSurface(
                    getDefaultDisplay(),
                    config,
                    configAttributes?.attrs,
                    0
                )

            override fun eglCreateWindowSurface(
                config: EGLConfig,
                surface: Surface,
                configAttributes: EGLConfigAttributes?,
            ): EGLSurface =
                EGL14.eglCreateWindowSurface(
                    getDefaultDisplay(),
                    config,
                    surface,
                    configAttributes?.attrs ?: DefaultWindowSurfaceConfig.attrs,
                    0
                )

            override fun eglSwapBuffers(surface: EGLSurface): Boolean =
                EGL14.eglSwapBuffers(getDefaultDisplay(), surface)

            override fun eglQuerySurface(
                surface: EGLSurface,
                attribute: Int,
                result: IntArray,
                offset: Int
            ): Boolean =
                EGL14.eglQuerySurface(getDefaultDisplay(), surface, attribute, result, offset)

            override fun eglDestroySurface(surface: EGLSurface) =
                EGL14.eglDestroySurface(getDefaultDisplay(), surface)

            override fun eglMakeCurrent(
                context: EGLContext,
                drawSurface: EGLSurface,
                readSurface: EGLSurface
            ): Boolean =
                EGL14.eglMakeCurrent(
                    getDefaultDisplay(),
                    drawSurface,
                    readSurface,
                    context
                )

            override fun loadConfig(configAttributes: EGLConfigAttributes): EGLConfig? {
                val configs = arrayOfNulls<EGLConfig?>(1)
                return if (EGL14.eglChooseConfig(
                    getDefaultDisplay(),
                    configAttributes.attrs,
                    0,
                    configs,
                    0,
                    1,
                    intArrayOf(1),
                    0
                )) {
                    configs[0]
                } else {
                    null
                }
            }

            override fun eglCreateContext(config: EGLConfig): EGLContext {
                return EGL14.eglCreateContext(
                    getDefaultDisplay(),
                    config,
                    EGL14.EGL_NO_CONTEXT, // not creating from a shared context
                    contextAttributes,
                    0
                )
            }

            override fun eglDestroyContext(eglContext: EGLContext) {
                if (!EGL14.eglDestroyContext(getDefaultDisplay(), eglContext)) {
                    throw EGLException(EGL14.eglGetError(), "Unable to destroy EGLContext")
                }
            }

            @RequiresApi(Build.VERSION_CODES.Q)
            override fun eglCreateImageFromHardwareBuffer(
                hardwareBuffer: HardwareBuffer
            ): EGLImageKHR? =
                EGLExt.eglCreateImageFromHardwareBuffer(getDefaultDisplay(), hardwareBuffer)

            override fun eglDestroyImageKHR(image: EGLImageKHR): Boolean =
                EGLExt.eglDestroyImageKHR(getDefaultDisplay(), image)

            override fun eglCreateSyncKHR(
                type: Int,
                attributes: EGLConfigAttributes?
            ): EGLSyncKHR? =
                EGLExt.eglCreateSyncKHR(getDefaultDisplay(), type, attributes)

            override fun eglGetSyncAttribKHR(
                sync: EGLSyncKHR,
                attribute: Int,
                value: IntArray,
                offset: Int
            ): Boolean =
                EGLExt.eglGetSyncAttribKHR(getDefaultDisplay(), sync, attribute, value, offset)

            override fun eglDestroySyncKHR(sync: EGLSyncKHR): Boolean =
                EGLExt.eglDestroySyncKHR(getDefaultDisplay(), sync)

            override fun eglGetError(): Int = EGL14.eglGetError()

            override fun eglClientWaitSyncKHR(
                sync: EGLSyncKHR,
                flags: Int,
                timeoutNanos: Long
            ): Int =
                EGLExt.eglClientWaitSyncKHR(getDefaultDisplay(), sync, flags, timeoutNanos)

            private fun getDefaultDisplay() = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)

            /**
             * EglConfigAttribute that provides the default attributes for an EGL window surface
             */
            private val DefaultWindowSurfaceConfig = EGLConfigAttributes {}
        }

        /**
         * Return a string representation of the corresponding EGL status code.
         * If the provided error value is not an EGL status code, the hex representation
         * is returned instead
         */
        @JvmStatic
        fun getStatusString(error: Int): String =
            when (error) {
                EGL14.EGL_SUCCESS -> "EGL_SUCCESS"
                EGL14.EGL_NOT_INITIALIZED -> "EGL_NOT_INITIALIZED"
                EGL14.EGL_BAD_ACCESS -> "EGL_BAD_ACCESS"
                EGL14.EGL_BAD_ALLOC -> "EGL_BAD_ALLOC"
                EGL14.EGL_BAD_ATTRIBUTE -> "EGL_BAD_ATTRIBUTE"
                EGL14.EGL_BAD_CONFIG -> "EGL_BAD_CONFIG"
                EGL14.EGL_BAD_CONTEXT -> "EGL_BAD_CONTEXT"
                EGL14.EGL_BAD_CURRENT_SURFACE -> "EGL_BAD_CURRENT_SURFACE"
                EGL14.EGL_BAD_DISPLAY -> "EGL_BAD_DISPLAY"
                EGL14.EGL_BAD_MATCH -> "EGL_BAD_MATCH"
                EGL14.EGL_BAD_NATIVE_PIXMAP -> "EGL_BAD_NATIVE_PIXMAP"
                EGL14.EGL_BAD_NATIVE_WINDOW -> "EGL_BAD_NATIVE_WINDOW"
                EGL14.EGL_BAD_PARAMETER -> "EGL_BAD_PARAMETER"
                EGL14.EGL_BAD_SURFACE -> "EGL_BAD_SURFACE"
                EGL14.EGL_CONTEXT_LOST -> "EGL_CONTEXT_LOST"
                else -> Integer.toHexString(error)
            }
    }
}

/**
 * Exception class for reporting errors with EGL
 *
 * @param error Error code reported via eglGetError
 * @param msg Optional message describing the exception being thrown
 */
@Suppress("AcronymName")
class EGLException(val error: Int, val msg: String = "") : RuntimeException() {

    override val message: String
        get() = "Error: ${EGLSpec.getStatusString(error)}, $msg"
}

/**
 * Identifier for the current EGL implementation
 *
 * @param major Major version of the EGL implementation
 * @param minor Minor version of the EGL implementation
 */
@Suppress("AcronymName")
data class EGLVersion(
    val major: Int,
    val minor: Int
) {

    override fun toString(): String {
        return "EGL version $major.$minor"
    }

    companion object {
        /**
         * Constant that represents version 1.4 of the EGL spec
         */
        @JvmField
        val V14 = EGLVersion(1, 4)

        /**
         * Constant that represents version 1.5 of the EGL spec
         */
        @JvmField
        val V15 = EGLVersion(1, 5)

        /**
         * Sentinel EglVersion value returned in error situations
         */
        @JvmField
        val Unknown = EGLVersion(-1, -1)
    }
}