GlContext.java

/*
 * Copyright 2023 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.camera.effects.opengl;

import static androidx.camera.effects.opengl.Utils.checkEglErrorOrLog;
import static androidx.camera.effects.opengl.Utils.checkEglErrorOrThrow;
import static androidx.camera.effects.opengl.Utils.drawArrays;
import static androidx.core.util.Preconditions.checkState;

import static java.util.Objects.requireNonNull;

import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.view.Surface;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.core.Logger;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Manages OpenGL configurations.
 *
 * <p>Allows registering and unregistering output Surfaces and manages their corresponding
 * {@link EGLSurface}.
 */
@RequiresApi(21)
public class GlContext {

    private static final String TAG = "GlContext";

    // EGL setup
    @Nullable
    private EGLDisplay mEglDisplay = EGL14.EGL_NO_DISPLAY;
    @Nullable
    private EGLContext mEglContext = EGL14.EGL_NO_CONTEXT;
    @Nullable
    private EGLConfig mEglConfig = null;

    // Current output Surface being drawn to.
    @Nullable
    private EglSurface mCurrentSurface = null;
    // A temporary output Surface. This is used when no Surface has been registered yet.
    @Nullable
    private EglSurface mTempSurface = null;
    @NonNull
    private final Map<Surface, EglSurface> mRegisteredSurfaces = new HashMap<>();

    @Nullable
    private Thread mGlThread;

    void init() {
        checkState(Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY), "Already initialized");
        mGlThread = Thread.currentThread();

        // Create EGLDisplay.
        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) {
            throw new IllegalStateException("Unable to get EGL14 display");
        }
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
            throw new IllegalStateException("Unable to initialize EGL14");
        }

        // Create EGLConfig.
        int rgbBits = 8;
        int alphaBits = 8;
        int renderType = EGL14.EGL_OPENGL_ES2_BIT;
        int recordableAndroid = EGL14.EGL_TRUE;
        int[] attribToChooseConfig = {
                EGL14.EGL_RED_SIZE, rgbBits,
                EGL14.EGL_GREEN_SIZE, rgbBits,
                EGL14.EGL_BLUE_SIZE, rgbBits,
                EGL14.EGL_ALPHA_SIZE, alphaBits,
                EGL14.EGL_DEPTH_SIZE, 0,
                EGL14.EGL_STENCIL_SIZE, 0,
                EGL14.EGL_RENDERABLE_TYPE, renderType,
                EGLExt.EGL_RECORDABLE_ANDROID, recordableAndroid,
                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT | EGL14.EGL_PBUFFER_BIT,
                EGL14.EGL_NONE
        };
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        if (!EGL14.eglChooseConfig(
                mEglDisplay, attribToChooseConfig, 0, configs, 0, configs.length, numConfigs, 0
        )) {
            throw new IllegalStateException("Unable to find a suitable EGLConfig");
        }
        mEglConfig = configs[0];
        int[] attribToCreateContext = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };

        // Create EGLContext.
        mEglContext = EGL14.eglCreateContext(
                mEglDisplay, mEglConfig, EGL14.EGL_NO_CONTEXT,
                attribToCreateContext, 0
        );
        checkEglErrorOrThrow("eglCreateContext");
        int[] values = new int[1];
        EGL14.eglQueryContext(
                mEglDisplay, mEglContext, EGL14.EGL_CONTEXT_CLIENT_VERSION, values, 0
        );
        Logger.d(TAG, "EGLContext created, client version " + values[0]);

        // Create a temporary surface to make it current.
        mTempSurface = create1x1PBufferSurface();
        makeCurrent(mTempSurface);
    }

    /**
     * Registers the given {@link Surface} as an output surface.
     *
     * <p>Once registered, the corresponding {@link EglSurface} can be used in
     * {@link #drawAndSwap}.
     */
    void registerSurface(@NonNull Surface surface) {
        checkInitialized();
        checkGlThread();
        if (!mRegisteredSurfaces.containsKey(surface)) {
            mRegisteredSurfaces.put(surface, null);
        }
    }

    /**
     * Unregisters the given {@link Surface} as an output surface.
     *
     * <p>Once unregistered, calling {@link #drawAndSwap} will no longer be effective.
     */
    void unregisterSurface(@NonNull Surface surface) {
        checkInitialized();
        checkGlThread();
        if (requireNonNull(mCurrentSurface).getSurface() == surface) {
            // If the current surface is being unregistered, switch to the temporary surface.
            makeCurrent(requireNonNull(mTempSurface));
        }
        // Destroy the EGLSurface.
        EglSurface removedSurface = mRegisteredSurfaces.remove(surface);
        if (removedSurface != null) {
            destroyEglSurface(removedSurface);
        }
    }

    /**
     * Draws the current bound texture to the given {@link Surface}.
     *
     * <p>No-ops if the given {@link Surface} is not registered.
     *
     * @param timestampNs The timestamp of the frame in nanoseconds.
     */
    void drawAndSwap(@NonNull Surface surface, long timestampNs) {
        checkInitialized();
        checkGlThread();
        checkState(mRegisteredSurfaces.containsKey(surface), "The Surface is not registered.");

        // Get or create the EGLSurface.
        EglSurface eglSurface = mRegisteredSurfaces.get(surface);
        // Workaround for when the output Surface is failed to create or needs to be recreated.
        if (eglSurface == null) {
            eglSurface = createEglSurface(surface);
            if (eglSurface == null) {
                Logger.w(TAG, "Failed to create EGLSurface. Skip drawing.");
                return;
            }
            mRegisteredSurfaces.put(surface, eglSurface);
        }

        // Draw.
        makeCurrent(eglSurface);
        drawArrays(eglSurface.getWidth(), eglSurface.getHeight());
        EGLExt.eglPresentationTimeANDROID(mEglDisplay, eglSurface.getEglSurface(), timestampNs);

        // Swap buffer
        if (!EGL14.eglSwapBuffers(mEglDisplay, eglSurface.getEglSurface())) {
            // If swap buffer failed, destroy the invalid EGL Surface.
            Logger.w(TAG, "Failed to swap buffers with EGL error: 0x" + Integer.toHexString(
                    EGL14.eglGetError()));
            unregisterSurface(surface);
            // Add the surface back since it's still registered.
            mRegisteredSurfaces.put(surface, null);
        }
    }

    void release() {
        checkGlThread();
        if (!Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) {
            EGL14.eglMakeCurrent(
                    mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                    EGL14.EGL_NO_CONTEXT
            );
        }

        // Destroy EGLSurfaces
        for (EglSurface eglSurface : mRegisteredSurfaces.values()) {
            if (eglSurface != null) {
                destroyEglSurface(eglSurface);
            }
        }
        mRegisteredSurfaces.clear();

        // Destroy the temporary surface.
        if (mTempSurface != null) {
            destroyEglSurface(mTempSurface);
            mTempSurface = null;
        }
        mCurrentSurface = null;

        // Destroy EGLContext and terminate display.
        if (!Objects.equals(mEglContext, EGL14.EGL_NO_CONTEXT)) {
            EGL14.eglDestroyContext(mEglDisplay, mEglContext);
            mEglContext = EGL14.EGL_NO_CONTEXT;
        }
        if (!Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) {
            EGL14.eglTerminate(mEglDisplay);
            mEglDisplay = EGL14.EGL_NO_DISPLAY;
        }

        EGL14.eglReleaseThread();
        mEglConfig = null;
    }

    // --- Private methods ---

    private void destroyEglSurface(@NonNull EglSurface eglSurface) {
        if (!EGL14.eglDestroySurface(mEglDisplay, eglSurface.getEglSurface())) {
            checkEglErrorOrLog("eglDestroySurface");
        }
    }

    @Nullable
    private EglSurface createEglSurface(@NonNull Surface surface) {
        EGLSurface eglSurface;
        try {
            int[] surfaceAttrib = {
                    EGL14.EGL_NONE
            };
            eglSurface = EGL14.eglCreateWindowSurface(
                    mEglDisplay, mEglConfig, surface, surfaceAttrib, 0);
            checkEglErrorOrThrow("eglCreateWindowSurface");
        } catch (IllegalStateException | IllegalArgumentException e) {
            Logger.w(TAG, "Failed to create EGL surface: " + e.getMessage(), e);
            return null;
        }
        int width = querySurface(eglSurface, EGL14.EGL_WIDTH);
        int height = querySurface(eglSurface, EGL14.EGL_HEIGHT);
        return EglSurface.of(eglSurface, surface, width, height);
    }

    private int querySurface(@NonNull EGLSurface eglSurface, int what) {
        int[] value = new int[1];
        EGL14.eglQuerySurface(mEglDisplay, eglSurface, what, value, 0);
        return value[0];
    }

    private void makeCurrent(@NonNull EglSurface eglSurface) {
        checkInitialized();
        if (!EGL14.eglMakeCurrent(mEglDisplay, eglSurface.getEglSurface(),
                eglSurface.getEglSurface(),
                mEglContext)) {
            throw new IllegalStateException(
                    "eglMakeCurrent failed. GL Error: " + EGL14.eglGetError());
        }

        mCurrentSurface = eglSurface;
    }

    private void checkInitialized() {
        checkState(isInitialized(), "GlContext is not initialized");
    }

    private void checkGlThread() {
        checkState(Objects.equals(mGlThread, Thread.currentThread()),
                "Must be called from the same thread that initializes this GlContext");
    }

    private boolean isInitialized() {
        return !Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)
                && !Objects.equals(mEglContext, EGL14.EGL_NO_CONTEXT)
                && mEglConfig != null;
    }

    private EglSurface create1x1PBufferSurface() {
        int width = 1;
        int height = 1;
        int[] surfaceAttrib = {
                EGL14.EGL_WIDTH, width,
                EGL14.EGL_HEIGHT, height,
                EGL14.EGL_NONE
        };
        EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEglDisplay, mEglConfig,
                surfaceAttrib, 0
        );
        checkEglErrorOrThrow("eglCreatePbufferSurface");
        if (eglSurface == null) {
            throw new IllegalStateException("surface was null");
        }
        return EglSurface.of(eglSurface, null, width, height);
    }
}