GlRenderer.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.checkGlErrorOrThrow;
import static androidx.camera.effects.opengl.Utils.configureExternalTexture;
import static androidx.camera.effects.opengl.Utils.configureTexture2D;
import static androidx.camera.effects.opengl.Utils.createTextureId;
import static androidx.core.util.Preconditions.checkState;

import android.graphics.Bitmap;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Build;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;

/**
 * A OpenGL render with a buffer queue and overlay support.
 *
 * <p>It allows 3 types of rendering:
 * <ul>
 * <li>Rendering the input texture directly to the output Surface,
 * <li>Rendering the input texture to a texture in the queue, and
 * <li>Rendering a texture in the queue to the output Surface.
 * </ul>
 *
 * <p>It also allows the caller to overlay a texture when rendering to Surface.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class GlRenderer {

    private static final String TAG = "GlRenderer";

    private boolean mInitialized = false;

    private Thread mGlThread = null;
    private final GlContext mGlContext = new GlContext();
    private final int mQueueDepth;
    private final GlProgramOverlay mGlProgramOverlay;
    private final GlProgramCopy mGlProgramCopy = new GlProgramCopy();

    // Texture IDs.
    private int mInputTextureId = -1;
    private int mOverlayTextureId = -1;
    @NonNull
    private int[] mQueueTextureIds = new int[0];
    private int mQueueTextureWidth = -1;
    private int mQueueTextureHeight = -1;

    // --- Public methods ---

    public GlRenderer(int queueDepth) {
        mQueueDepth = queueDepth;
        mGlProgramOverlay = new GlProgramOverlay(queueDepth);
    }

    /**
     * Initializes the renderer.
     *
     * <p>Must be called before any other methods.
     */
    public void init() {
        checkState(!mInitialized, "Already initialized");
        mInitialized = true;
        mGlThread = Thread.currentThread();
        try {
            mGlContext.init();
            mGlProgramCopy.init();
            mGlProgramOverlay.init();
            mInputTextureId = createTextureId();
            configureExternalTexture(mInputTextureId);
            mOverlayTextureId = createTextureId();
            configureExternalTexture(mOverlayTextureId);
        } catch (IllegalStateException | IllegalArgumentException e) {
            release();
            throw e;
        }
    }

    /**
     * Releases the renderer.
     *
     * <p>Once released, it can never be accessed again.
     */
    public void release() {
        checkGlThreadAndInitialized();

        mInitialized = false;
        mGlThread = null;
        mQueueTextureWidth = -1;
        mQueueTextureHeight = -1;

        mGlContext.release();
        mGlProgramOverlay.release();
        mGlProgramCopy.release();

        if (mInputTextureId != -1) {
            GLES20.glDeleteTextures(1, new int[]{mInputTextureId}, 0);
            checkGlErrorOrThrow("glDeleteTextures");
            mInputTextureId = -1;
        }
        if (mOverlayTextureId != -1) {
            GLES20.glDeleteTextures(1, new int[]{mOverlayTextureId}, 0);
            checkGlErrorOrThrow("glDeleteTextures");
            mOverlayTextureId = -1;
        }
        if (mQueueTextureIds.length > 0) {
            GLES20.glDeleteTextures(mQueueTextureIds.length, mQueueTextureIds, 0);
            checkGlErrorOrThrow("glDeleteTextures");
            mQueueTextureIds = new int[0];
        }
    }

    /**
     * Gets the external input texture ID created during initialization.
     */
    public int getInputTextureId() {
        checkGlThreadAndInitialized();
        return mInputTextureId;
    }

    /**
     * Gets the external overlay texture ID created during initialization.
     */
    public int getOverlayTextureId() {
        checkGlThreadAndInitialized();
        return mOverlayTextureId;
    }

    /**
     * Creates an array of textures and return.
     *
     * <p>This method creates an array of {@link GLES20#GL_TEXTURE_2D} textures with the
     * {@link #mQueueDepth} from constructor, and return their IDs. If the array already exists,
     * calling this method deletes the current array before creating a new one.
     *
     * @param size the size of the texture in this queue. The size usually matches the size
     *             of the input texture.
     */
    @NonNull
    public int[] createBufferTextureIds(@NonNull Size size) {
        checkGlThreadAndInitialized();
        // Delete the current buffer if it exists.
        if (mQueueTextureIds.length > 0) {
            GLES20.glDeleteTextures(mQueueTextureIds.length, mQueueTextureIds, 0);
            checkGlErrorOrThrow("glDeleteTextures");
        }

        mQueueTextureIds = new int[mQueueDepth];
        // If the queue depth is 0, return an empty array. There is no need to create textures.
        if (mQueueDepth == 0) {
            return mQueueTextureIds;
        }

        // Create the textures.
        GLES20.glGenTextures(mQueueDepth, mQueueTextureIds, 0);
        checkGlErrorOrThrow("glGenTextures");
        mQueueTextureWidth = size.getWidth();
        mQueueTextureHeight = size.getHeight();
        for (int textureId : mQueueTextureIds) {
            configureTexture2D(textureId);
            GLES20.glTexImage2D(
                    GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGB, size.getWidth(), size.getHeight(), 0,
                    GLES20.GL_RGB,
                    GLES20.GL_UNSIGNED_BYTE,
                    null
            );
        }
        return mQueueTextureIds;
    }

    /**
     * Registers an output surface.
     *
     * <p>Once registered, the {@link Surface} can be used by {@link #renderInputToSurface} and
     * {@link #renderQueueTextureToSurface}.
     */
    public void registerOutputSurface(@NonNull Surface surface) {
        checkGlThreadAndInitialized();
        mGlContext.registerSurface(surface);
    }

    /**
     * Unregisters the output surface.
     *
     * <p>Once unregistered, calling {@link #renderInputToSurface} or
     * {@link #renderQueueTextureToSurface} with the {@link Surface} throws an exception.
     */
    public void unregisterOutputSurface(@NonNull Surface surface) {
        checkGlThreadAndInitialized();
        mGlContext.unregisterSurface(surface);
    }

    /**
     * Renders the input texture directly to the output surface.
     *
     * <p>This is used when the queue depth is 0 and no buffer copy is needed. The surface must
     * be registered via {@link #registerOutputSurface}.
     */
    public void renderInputToSurface(long timestampNs, @NonNull float[] textureTransform,
            @NonNull Surface surface) {
        checkGlThreadAndInitialized();
        checkState(mQueueDepth == 0, "Queue depth must be zero");
        mGlProgramOverlay.draw(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mInputTextureId,
                mOverlayTextureId, textureTransform, mGlContext, surface, timestampNs);
    }

    /**
     * Renders a queued texture to the output surface.
     *
     * <p>Draw the content of the texture to the output surface. The surface must be registered via
     * {@link #registerOutputSurface}. The texture ID must be from the latest return value of
     * {@link #createBufferTextureIds}.
     */
    public void renderQueueTextureToSurface(int textureId, long timestampNs,
            @NonNull float[] textureTransform, @NonNull Surface surface) {
        checkGlThreadAndInitialized();
        checkState(mQueueDepth > 0, "Queue depth must be non-zero");
        mGlProgramOverlay.draw(GLES20.GL_TEXTURE_2D, textureId, mOverlayTextureId,
                textureTransform, mGlContext, surface, timestampNs);
    }

    /**
     * Renders the input texture to a texture in the queue.
     *
     * <p>The texture ID must be from the latest return value of{@link #createBufferTextureIds}.
     */
    public void renderInputToQueueTexture(int textureId) {
        checkGlThreadAndInitialized();
        mGlProgramCopy.draw(mInputTextureId, textureId, mQueueTextureWidth, mQueueTextureHeight);
    }

    /**
     * Renders a queued texture to a Bitmap and returns.
     */
    @NonNull
    public Bitmap renderQueueTextureToBitmap(int textureId, int width, int height,
            @NonNull float[] textureTransform) {
        checkGlThreadAndInitialized();
        checkState(mQueueDepth > 0, "Queue depth must be non-zero");
        return mGlProgramOverlay.snapshot(GLES20.GL_TEXTURE_2D, textureId, mOverlayTextureId,
                width, height, textureTransform);
    }

    /**
     * Renders the input texture to a Bitmap and returns.
     */
    @NonNull
    public Bitmap renderInputToBitmap(int width, int height, @NonNull float[] textureTransform) {
        checkGlThreadAndInitialized();
        checkState(mQueueDepth == 0, "Queue depth must be zero");
        return mGlProgramOverlay.snapshot(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mInputTextureId,
                mOverlayTextureId, width, height, textureTransform);
    }

    // --- Private methods ---

    private void checkGlThreadAndInitialized() {
        checkState(mInitialized, "OpenGlRenderer is not initialized");
        checkState(mGlThread == Thread.currentThread(),
                "Method call must be called on the GL thread.");
    }
}