Adopt EglThread in EglRenderer

This allows EglRenderer to be able to share render thread and EGLContext
with others.
go/meet-android-eglcontext-reduction

Bug: b/225229697
Change-Id: I896c8082ef8b64f5b544fa2eda7303fbca3985d1
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/316881
Reviewed-by: Xavier Lepaul‎ <xalep@webrtc.org>
Commit-Queue: Linus Nilsson <lnilsson@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40574}
This commit is contained in:
Linus Nilsson 2023-08-18 14:15:33 +02:00 committed by WebRTC LUCI CQ
parent 47f4e55612
commit ad3f1bcc1b

View file

@ -14,11 +14,8 @@ import android.graphics.Bitmap;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@ -86,35 +83,20 @@ public class EglRenderer implements VideoSink {
} }
} }
/**
* Handler that triggers a callback when an uncaught exception happens when handling a message.
*/
private static class HandlerWithExceptionCallback extends Handler {
private final Runnable exceptionCallback;
public HandlerWithExceptionCallback(Looper looper, Runnable exceptionCallback) {
super(looper);
this.exceptionCallback = exceptionCallback;
}
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {
Logging.e(TAG, "Exception on EglRenderer thread", e);
exceptionCallback.run();
throw e;
}
}
}
protected final String name; protected final String name;
// `renderThreadHandler` is a handler for communicating with `renderThread`, and is synchronized // `eglThread` is used for rendering, and is synchronized on `threadLock`.
// on `handlerLock`. private final Object threadLock = new Object();
private final Object handlerLock = new Object(); @GuardedBy("threadLock") @Nullable private EglThread eglThread;
@Nullable private Handler renderThreadHandler;
private final Runnable eglExceptionCallback = new Runnable() {
@Override
public void run() {
synchronized (threadLock) {
eglThread = null;
}
}
};
private final ArrayList<FrameListenerAndParams> frameListeners = new ArrayList<>(); private final ArrayList<FrameListenerAndParams> frameListeners = new ArrayList<>();
@ -172,10 +154,10 @@ public class EglRenderer implements VideoSink {
@Override @Override
public void run() { public void run() {
logStatistics(); logStatistics();
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler != null) { if (eglThread != null) {
renderThreadHandler.removeCallbacks(logStatisticsRunnable); eglThread.getHandler().removeCallbacks(logStatisticsRunnable);
renderThreadHandler.postDelayed( eglThread.getHandler().postDelayed(
logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC)); logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
} }
} }
@ -185,8 +167,8 @@ public class EglRenderer implements VideoSink {
private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation(); private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation();
/** /**
* Standard constructor. The name will be used for the render thread name and included when * Standard constructor. The name will be included when logging. In order to render something,
* logging. In order to render something, you must first call init() and createEglSurface. * you must first call init() and createEglSurface.
*/ */
public EglRenderer(String name) { public EglRenderer(String name) {
this(name, new VideoFrameDrawer()); this(name, new VideoFrameDrawer());
@ -197,6 +179,31 @@ public class EglRenderer implements VideoSink {
this.frameDrawer = videoFrameDrawer; this.frameDrawer = videoFrameDrawer;
} }
public void init(
EglThread eglThread, RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) {
synchronized (threadLock) {
if (this.eglThread != null) {
throw new IllegalStateException(name + "Already initialized");
}
logD("Initializing EglRenderer");
this.eglThread = eglThread;
this.drawer = drawer;
this.usePresentationTimeStamp = usePresentationTimeStamp;
eglThread.addExceptionCallback(eglExceptionCallback);
eglBase = eglThread.createEglBaseWithSharedConnection();
eglThread.getHandler().post(eglSurfaceCreationRunnable);
final long currentTimeNs = System.nanoTime();
resetStatistics(currentTimeNs);
eglThread.getHandler().postDelayed(
logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
}
}
/** /**
* Initialize this class, sharing resources with `sharedContext`. The custom `drawer` will be used * Initialize this class, sharing resources with `sharedContext`. The custom `drawer` will be used
* for drawing frames on the EGLSurface. This class is responsible for calling release() on * for drawing frames on the EGLSurface. This class is responsible for calling release() on
@ -207,46 +214,9 @@ public class EglRenderer implements VideoSink {
*/ */
public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes, public void init(@Nullable final EglBase.Context sharedContext, final int[] configAttributes,
RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) { RendererCommon.GlDrawer drawer, boolean usePresentationTimeStamp) {
synchronized (handlerLock) { EglThread thread =
if (renderThreadHandler != null) { EglThread.create(/* releaseMonitor= */ null, sharedContext, configAttributes);
throw new IllegalStateException(name + "Already initialized"); init(thread, drawer, usePresentationTimeStamp);
}
logD("Initializing EglRenderer");
this.drawer = drawer;
this.usePresentationTimeStamp = usePresentationTimeStamp;
final HandlerThread renderThread = new HandlerThread(name + "EglRenderer");
renderThread.start();
renderThreadHandler =
new HandlerWithExceptionCallback(renderThread.getLooper(), new Runnable() {
@Override
public void run() {
synchronized (handlerLock) {
renderThreadHandler = null;
}
}
});
// Create EGL context on the newly created render thread. It should be possibly to create the
// context on this thread and make it current on the render thread, but this causes failure on
// some Marvel based JB devices. https://bugs.chromium.org/p/webrtc/issues/detail?id=6350.
ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
// If sharedContext is null, then texture frames are disabled. This is typically for old
// devices that might not be fully spec compliant, so force EGL 1.0 since EGL 1.4 has
// caused trouble on some weird devices.
if (sharedContext == null) {
logD("EglBase10.create context");
eglBase = EglBase.createEgl10(configAttributes);
} else {
logD("EglBase.create shared context");
eglBase = EglBase.create(sharedContext, configAttributes);
}
});
renderThreadHandler.post(eglSurfaceCreationRunnable);
final long currentTimeNs = System.nanoTime();
resetStatistics(currentTimeNs);
renderThreadHandler.postDelayed(
logStatisticsRunnable, TimeUnit.SECONDS.toMillis(LOG_INTERVAL_SEC));
}
} }
/** /**
@ -281,14 +251,16 @@ public class EglRenderer implements VideoSink {
public void release() { public void release() {
logD("Releasing."); logD("Releasing.");
final CountDownLatch eglCleanupBarrier = new CountDownLatch(1); final CountDownLatch eglCleanupBarrier = new CountDownLatch(1);
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler == null) { if (eglThread == null) {
logD("Already released"); logD("Already released");
return; return;
} }
renderThreadHandler.removeCallbacks(logStatisticsRunnable); eglThread.getHandler().removeCallbacks(logStatisticsRunnable);
eglThread.removeExceptionCallback(eglExceptionCallback);
// Release EGL and GL resources on render thread. // Release EGL and GL resources on render thread.
renderThreadHandler.postAtFrontOfQueue(() -> { eglThread.getHandler().postAtFrontOfQueue(() -> {
// Detach current shader program. // Detach current shader program.
synchronized (EglBase.lock) { synchronized (EglBase.lock) {
GLES20.glUseProgram(/* program= */ 0); GLES20.glUseProgram(/* program= */ 0);
@ -299,23 +271,19 @@ public class EglRenderer implements VideoSink {
} }
frameDrawer.release(); frameDrawer.release();
bitmapTextureFramebuffer.release(); bitmapTextureFramebuffer.release();
if (eglBase != null) { if (eglBase != null) {
logD("eglBase detach and release.");
eglBase.detachCurrent();
eglBase.release(); eglBase.release();
eglBase = null; eglBase = null;
} }
frameListeners.clear(); frameListeners.clear();
eglCleanupBarrier.countDown(); eglCleanupBarrier.countDown();
}); });
final Looper renderLooper = renderThreadHandler.getLooper();
// TODO(magjed): Replace this post() with renderLooper.quitSafely() when API support >= 18.
renderThreadHandler.post(() -> {
logD("Quitting render thread.");
renderLooper.quit();
});
// Don't accept any more frames or messages to the render thread. // Don't accept any more frames or messages to the render thread.
renderThreadHandler = null; eglThread.release();
eglThread = null;
} }
// Make sure the EGL/GL cleanup posted above is executed. // Make sure the EGL/GL cleanup posted above is executed.
ThreadUtils.awaitUninterruptibly(eglCleanupBarrier); ThreadUtils.awaitUninterruptibly(eglCleanupBarrier);
@ -343,9 +311,9 @@ public class EglRenderer implements VideoSink {
} }
public void printStackTrace() { public void printStackTrace() {
synchronized (handlerLock) { synchronized (threadLock) {
final Thread renderThread = final Thread renderThread =
(renderThreadHandler == null) ? null : renderThreadHandler.getLooper().getThread(); (eglThread == null) ? null : eglThread.getHandler().getLooper().getThread();
if (renderThread != null) { if (renderThread != null) {
final StackTraceElement[] renderStackTrace = renderThread.getStackTrace(); final StackTraceElement[] renderStackTrace = renderThread.getStackTrace();
if (renderStackTrace.length > 0) { if (renderStackTrace.length > 0) {
@ -475,11 +443,11 @@ public class EglRenderer implements VideoSink {
*/ */
public void removeFrameListener(final FrameListener listener) { public void removeFrameListener(final FrameListener listener) {
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler == null) { if (eglThread == null) {
return; return;
} }
if (Thread.currentThread() == renderThreadHandler.getLooper().getThread()) { if (Thread.currentThread() == eglThread.getHandler().getLooper().getThread()) {
throw new RuntimeException("removeFrameListener must not be called on the render thread."); throw new RuntimeException("removeFrameListener must not be called on the render thread.");
} }
postToRenderThread(() -> { postToRenderThread(() -> {
@ -507,8 +475,8 @@ public class EglRenderer implements VideoSink {
++framesReceived; ++framesReceived;
} }
final boolean dropOldFrame; final boolean dropOldFrame;
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler == null) { if (eglThread == null) {
logD("Dropping frame - Not initialized or already released."); logD("Dropping frame - Not initialized or already released.");
return; return;
} }
@ -519,7 +487,7 @@ public class EglRenderer implements VideoSink {
} }
pendingFrame = frame; pendingFrame = frame;
pendingFrame.retain(); pendingFrame.retain();
renderThreadHandler.post(this ::renderFrameOnRenderThread); eglThread.getHandler().post(this::renderFrameOnRenderThread);
} }
} }
if (dropOldFrame) { if (dropOldFrame) {
@ -536,12 +504,11 @@ public class EglRenderer implements VideoSink {
// Ensure that the render thread is no longer touching the Surface before returning from this // Ensure that the render thread is no longer touching the Surface before returning from this
// function. // function.
eglSurfaceCreationRunnable.setSurface(null /* surface */); eglSurfaceCreationRunnable.setSurface(null /* surface */);
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler != null) { if (eglThread != null) {
renderThreadHandler.removeCallbacks(eglSurfaceCreationRunnable); eglThread.getHandler().removeCallbacks(eglSurfaceCreationRunnable);
renderThreadHandler.postAtFrontOfQueue(() -> { eglThread.getHandler().postAtFrontOfQueue(() -> {
if (eglBase != null) { if (eglBase != null) {
eglBase.detachCurrent();
eglBase.releaseSurface(); eglBase.releaseSurface();
} }
completionCallback.run(); completionCallback.run();
@ -556,9 +523,9 @@ public class EglRenderer implements VideoSink {
* Private helper function to post tasks safely. * Private helper function to post tasks safely.
*/ */
private void postToRenderThread(Runnable runnable) { private void postToRenderThread(Runnable runnable) {
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler != null) { if (eglThread != null) {
renderThreadHandler.post(runnable); eglThread.getHandler().post(runnable);
} }
} }
} }
@ -566,6 +533,7 @@ public class EglRenderer implements VideoSink {
private void clearSurfaceOnRenderThread(float r, float g, float b, float a) { private void clearSurfaceOnRenderThread(float r, float g, float b, float a) {
if (eglBase != null && eglBase.hasSurface()) { if (eglBase != null && eglBase.hasSurface()) {
logD("clearSurface"); logD("clearSurface");
eglBase.makeCurrent();
GLES20.glClearColor(r, g, b, a); GLES20.glClearColor(r, g, b, a);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
eglBase.swapBuffers(); eglBase.swapBuffers();
@ -583,11 +551,11 @@ public class EglRenderer implements VideoSink {
* Post a task to clear the surface to a specific color. * Post a task to clear the surface to a specific color.
*/ */
public void clearImage(final float r, final float g, final float b, final float a) { public void clearImage(final float r, final float g, final float b, final float a) {
synchronized (handlerLock) { synchronized (threadLock) {
if (renderThreadHandler == null) { if (eglThread == null) {
return; return;
} }
renderThreadHandler.postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a)); eglThread.getHandler().postAtFrontOfQueue(() -> clearSurfaceOnRenderThread(r, g, b, a));
} }
} }
@ -609,6 +577,8 @@ public class EglRenderer implements VideoSink {
frame.release(); frame.release();
return; return;
} }
eglBase.makeCurrent();
// Check if fps reduction is active. // Check if fps reduction is active.
final boolean shouldRenderFrame; final boolean shouldRenderFrame;
synchronized (fpsReductionLock) { synchronized (fpsReductionLock) {