From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../java/org/mozilla/gecko/gfx/AndroidVsync.java | 72 +++++ .../gecko/gfx/CompositorSurfaceManager.java | 26 ++ .../java/org/mozilla/gecko/gfx/GeckoSurface.java | 152 ++++++++++ .../org/mozilla/gecko/gfx/GeckoSurfaceTexture.java | 330 +++++++++++++++++++++ .../java/org/mozilla/gecko/gfx/PanningPerfAPI.java | 71 +++++ .../mozilla/gecko/gfx/RemoteSurfaceAllocator.java | 77 +++++ .../org/mozilla/gecko/gfx/SurfaceAllocator.java | 143 +++++++++ .../mozilla/gecko/gfx/SurfaceControlManager.java | 105 +++++++ .../mozilla/gecko/gfx/SurfaceTextureListener.java | 38 +++ .../java/org/mozilla/gecko/gfx/SyncConfig.java | 59 ++++ 10 files changed, 1073 insertions(+) create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx') diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java new file mode 100644 index 0000000000..c87bf466d0 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java @@ -0,0 +1,72 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; + +/** This class receives HW vsync events through a {@link Choreographer}. */ +@WrapForJNI +/* package */ final class AndroidVsync extends JNIObject implements Choreographer.FrameCallback { + @WrapForJNI + @Override // JNIObject + protected native void disposeNative(); + + private static final String LOGTAG = "AndroidVsync"; + + /* package */ Choreographer mChoreographer; + private volatile boolean mObservingVsync; + + public AndroidVsync() { + final Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post( + new Runnable() { + @Override + public void run() { + mChoreographer = Choreographer.getInstance(); + if (mObservingVsync) { + mChoreographer.postFrameCallback(AndroidVsync.this); + } + } + }); + } + + @WrapForJNI(stubName = "NotifyVsync") + private native void nativeNotifyVsync(final long frameTimeNanos); + + // Choreographer callback implementation. + public void doFrame(final long frameTimeNanos) { + if (mObservingVsync) { + mChoreographer.postFrameCallback(this); + nativeNotifyVsync(frameTimeNanos); + } + } + + /** + * Start/stop observing Vsync event. + * + * @param enable true to start observing; false to stop. + * @return true if observing and false if not. + */ + @WrapForJNI + public synchronized boolean observeVsync(final boolean enable) { + if (mObservingVsync != enable) { + mObservingVsync = enable; + + if (mChoreographer != null) { + if (enable) { + mChoreographer.postFrameCallback(this); + } else { + mChoreographer.removeFrameCallback(this); + } + } + } + return mObservingVsync; + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java new file mode 100644 index 0000000000..1378a284b7 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java @@ -0,0 +1,26 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.RemoteException; +import android.view.Surface; +import org.mozilla.gecko.annotation.WrapForJNI; + +public final class CompositorSurfaceManager { + private static final String LOGTAG = "CompSurfManager"; + + private ICompositorSurfaceManager mManager; + + public CompositorSurfaceManager(final ICompositorSurfaceManager aManager) { + mManager = aManager; + } + + @WrapForJNI(exceptionMode = "nsresult") + public synchronized void onSurfaceChanged(final int widgetId, final Surface surface) + throws RemoteException { + mManager.onSurfaceChanged(widgetId, surface); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java new file mode 100644 index 0000000000..7cf891aa59 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java @@ -0,0 +1,152 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import static org.mozilla.geckoview.BuildConfig.DEBUG_BUILD; + +import android.os.Parcel; +import android.os.Parcelable; +import android.view.Surface; +import org.mozilla.gecko.annotation.WrapForJNI; + +public final class GeckoSurface implements Parcelable { + private static final String LOGTAG = "GeckoSurface"; + + private Surface mSurface; + private long mHandle; + private boolean mIsSingleBuffer; + private volatile boolean mIsAvailable; + private boolean mOwned = true; + private volatile boolean mIsReleased = false; + + private int mMyPid; + // Locally allocated surface/texture. Do not pass it over IPC. + private GeckoSurface mSyncSurface; + + @WrapForJNI(exceptionMode = "nsresult") + public GeckoSurface(final GeckoSurfaceTexture gst) { + mSurface = new Surface(gst); + mHandle = gst.getHandle(); + mIsSingleBuffer = gst.isSingleBuffer(); + mIsAvailable = true; + mMyPid = android.os.Process.myPid(); + } + + public GeckoSurface(final Parcel p) { + mSurface = Surface.CREATOR.createFromParcel(p); + mHandle = p.readLong(); + mIsSingleBuffer = p.readByte() == 1 ? true : false; + mIsAvailable = (p.readByte() == 1 ? true : false); + mMyPid = p.readInt(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public GeckoSurface createFromParcel(final Parcel p) { + return new GeckoSurface(p); + } + + public GeckoSurface[] newArray(final int size) { + return new GeckoSurface[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel out, final int flags) { + mSurface.writeToParcel(out, flags); + if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) == 0) { + // GeckoSurface can be passed across processes as a return value or + // an argument, and should always tranfers its ownership (move) to + // the receiver of parcel. On the other hand, Surface is moved only + // when passed as a return value and releases itself when corresponding + // write flags is provided. (See Surface.writeToParcel().) + // The superclass method must be called here to ensure the local instance + // is truely forgotten. + mSurface.release(); + } + mOwned = false; + + out.writeLong(mHandle); + out.writeByte((byte) (mIsSingleBuffer ? 1 : 0)); + out.writeByte((byte) (mIsAvailable ? 1 : 0)); + out.writeInt(mMyPid); + } + + public void release() { + if (mIsReleased) { + return; + } + mIsReleased = true; + + if (mSyncSurface != null) { + mSyncSurface.release(); + final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(mSyncSurface.getHandle()); + if (gst != null) { + gst.decrementUse(); + } + mSyncSurface = null; + } + + if (mOwned) { + mSurface.release(); + } + } + + @WrapForJNI + public long getHandle() { + return mHandle; + } + + @WrapForJNI + public Surface getSurface() { + return mSurface; + } + + @WrapForJNI + public boolean getAvailable() { + return mIsAvailable; + } + + @WrapForJNI + public boolean isReleased() { + return mIsReleased; + } + + @WrapForJNI + public void setAvailable(final boolean available) { + mIsAvailable = available; + } + + /* package */ boolean inProcess() { + return android.os.Process.myPid() == mMyPid; + } + + /* package */ SyncConfig initSyncSurface(final int width, final int height) { + if (DEBUG_BUILD) { + if (inProcess()) { + throw new AssertionError("no need for sync when allocated in process"); + } + } + if (GeckoSurfaceTexture.lookup(mHandle) != null) { + throw new AssertionError("texture#" + mHandle + " already in use."); + } + final GeckoSurfaceTexture texture = + GeckoSurfaceTexture.acquire(GeckoSurfaceTexture.isSingleBufferSupported(), mHandle); + if (texture != null) { + texture.setDefaultBufferSize(width, height); + texture.track(mHandle); + mSyncSurface = new GeckoSurface(texture); + return new SyncConfig(mHandle, mSyncSurface, width, height); + } + + return null; + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java new file mode 100644 index 0000000000..2d045edb44 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java @@ -0,0 +1,330 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.util.Log; +import android.util.LongSparseArray; +import androidx.annotation.RequiresApi; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicInteger; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; + +/* package */ final class GeckoSurfaceTexture extends SurfaceTexture { + private static final String LOGTAG = "GeckoSurfaceTexture"; + private static final int MAX_SURFACE_TEXTURES = 200; + private static final LongSparseArray sSurfaceTextures = + new LongSparseArray(); + + private static LongSparseArray> sUnusedTextures = + new LongSparseArray>(); + + private long mHandle; + private boolean mIsSingleBuffer; + + private long mAttachedContext; + private int mTexName; + + private GeckoSurfaceTexture.Callbacks mListener; + private AtomicInteger mUseCount; + private boolean mFinalized; + + private long mUpstream; + private NativeGLBlitHelper mBlitter; + + private GeckoSurfaceTexture(final long handle) { + super(0); + init(handle, false); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private GeckoSurfaceTexture(final long handle, final boolean singleBufferMode) { + super(0, singleBufferMode); + init(handle, singleBufferMode); + } + + @Override + protected void finalize() throws Throwable { + // We only want finalize() to be called once + if (mFinalized) { + return; + } + + mFinalized = true; + super.finalize(); + } + + private void init(final long handle, final boolean singleBufferMode) { + mHandle = handle; + mIsSingleBuffer = singleBufferMode; + mUseCount = new AtomicInteger(1); + + // Start off detached + detachFromGLContext(); + } + + @WrapForJNI + public long getHandle() { + return mHandle; + } + + @WrapForJNI + public int getTexName() { + return mTexName; + } + + @WrapForJNI(exceptionMode = "nsresult") + public synchronized void attachToGLContext(final long context, final int texName) { + if (context == mAttachedContext && texName == mTexName) { + return; + } + + attachToGLContext(texName); + + mAttachedContext = context; + mTexName = texName; + } + + @Override + @WrapForJNI(exceptionMode = "nsresult") + public synchronized void detachFromGLContext() { + super.detachFromGLContext(); + + mAttachedContext = mTexName = 0; + } + + @WrapForJNI + public synchronized boolean isAttachedToGLContext(final long context) { + return mAttachedContext == context; + } + + @WrapForJNI + public boolean isSingleBuffer() { + return mIsSingleBuffer; + } + + @Override + @WrapForJNI + public synchronized void updateTexImage() { + try { + if (mUpstream != 0) { + SurfaceAllocator.sync(mUpstream); + } + super.updateTexImage(); + if (mListener != null) { + mListener.onUpdateTexImage(); + } + } catch (final Exception e) { + Log.w(LOGTAG, "updateTexImage() failed", e); + } + } + + @Override + public synchronized void release() { + mUpstream = 0; + if (mBlitter != null) { + mBlitter.close(); + } + try { + super.release(); + synchronized (sSurfaceTextures) { + sSurfaceTextures.remove(mHandle); + } + } catch (final Exception e) { + Log.w(LOGTAG, "release() failed", e); + } + } + + @Override + @WrapForJNI + public synchronized void releaseTexImage() { + if (!mIsSingleBuffer) { + return; + } + + try { + super.releaseTexImage(); + if (mListener != null) { + mListener.onReleaseTexImage(); + } + } catch (final Exception e) { + Log.w(LOGTAG, "releaseTexImage() failed", e); + } + } + + public synchronized void setListener(final GeckoSurfaceTexture.Callbacks listener) { + mListener = listener; + } + + @WrapForJNI + public static boolean isSingleBufferSupported() { + return Build.VERSION.SDK_INT >= 19; + } + + @WrapForJNI + public synchronized void incrementUse() { + mUseCount.incrementAndGet(); + } + + @WrapForJNI + public synchronized void decrementUse() { + final int useCount = mUseCount.decrementAndGet(); + + if (useCount == 0) { + setListener(null); + + if (mAttachedContext == 0) { + release(); + synchronized (sUnusedTextures) { + sSurfaceTextures.remove(mHandle); + } + return; + } + + synchronized (sUnusedTextures) { + LinkedList list = sUnusedTextures.get(mAttachedContext); + if (list == null) { + list = new LinkedList(); + sUnusedTextures.put(mAttachedContext, list); + } + list.addFirst(this); + } + } + } + + @WrapForJNI + public static void destroyUnused(final long context) { + final LinkedList list; + synchronized (sUnusedTextures) { + list = sUnusedTextures.get(context); + sUnusedTextures.delete(context); + } + + if (list == null) { + return; + } + + for (final GeckoSurfaceTexture tex : list) { + try { + if (tex.isSingleBuffer()) { + tex.releaseTexImage(); + } + + tex.detachFromGLContext(); + tex.release(); + + // We need to manually call finalize here, otherwise we can run out + // of file descriptors if the GC doesn't kick in soon enough. Bug 1416015. + try { + tex.finalize(); + } catch (final Throwable t) { + Log.e(LOGTAG, "Failed to finalize SurfaceTexture", t); + } + } catch (final Exception e) { + Log.e(LOGTAG, "Failed to destroy SurfaceTexture", e); + } + } + } + + public static GeckoSurfaceTexture acquire(final boolean singleBufferMode, final long handle) { + if (singleBufferMode && !isSingleBufferSupported()) { + throw new IllegalArgumentException("single buffer mode not supported on API version < 19"); + } + + // Attempting to create a SurfaceTexture from an isolated process on Android versions prior to + // 8.0 results in an indefinite hang. See bug 1706656. + if (GeckoAppShell.isIsolatedProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return null; + } + + synchronized (sSurfaceTextures) { + // We want to limit the maximum number of SurfaceTextures at any one time. + // This is because they use a large number of fds, and once the process' limit + // is reached bad things happen. See bug 1421586. + if (sSurfaceTextures.size() >= MAX_SURFACE_TEXTURES) { + return null; + } + + if (sSurfaceTextures.indexOfKey(handle) >= 0) { + throw new IllegalArgumentException("Already have a GeckoSurfaceTexture with that handle"); + } + + final GeckoSurfaceTexture gst; + if (isSingleBufferSupported()) { + gst = new GeckoSurfaceTexture(handle, singleBufferMode); + } else { + gst = new GeckoSurfaceTexture(handle); + } + + sSurfaceTextures.put(handle, gst); + + return gst; + } + } + + @WrapForJNI + public static GeckoSurfaceTexture lookup(final long handle) { + synchronized (sSurfaceTextures) { + return sSurfaceTextures.get(handle); + } + } + + /* package */ synchronized void track(final long upstream) { + mUpstream = upstream; + } + + /* package */ synchronized void configureSnapshot( + final GeckoSurface target, final int width, final int height) { + mBlitter = NativeGLBlitHelper.create(mHandle, target, width, height); + } + + /* package */ synchronized void takeSnapshot() { + mBlitter.blit(); + } + + public interface Callbacks { + void onUpdateTexImage(); + + void onReleaseTexImage(); + } + + @WrapForJNI + public static final class NativeGLBlitHelper extends JNIObject { + public static NativeGLBlitHelper create( + final long textureHandle, + final GeckoSurface targetSurface, + final int width, + final int height) { + final NativeGLBlitHelper helper = nativeCreate(textureHandle, targetSurface, width, height); + helper.mTargetSurface = targetSurface; // Take ownership of surface. + return helper; + } + + public static native NativeGLBlitHelper nativeCreate( + final long textureHandle, + final GeckoSurface targetSurface, + final int width, + final int height); + + public native void blit(); + + public void close() { + disposeNative(); + if (mTargetSurface != null) { + mTargetSurface.release(); + mTargetSurface = null; + } + } + + @Override + protected native void disposeNative(); + + private GeckoSurface mTargetSurface; + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java new file mode 100644 index 0000000000..b8ceb74f0b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java @@ -0,0 +1,71 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.SystemClock; +import android.util.Log; +import java.util.ArrayList; +import java.util.List; +import org.mozilla.gecko.annotation.RobocopTarget; + +public final class PanningPerfAPI { + private static final String LOGTAG = "GeckoPanningPerfAPI"; + + // make this large enough to avoid having to resize the frame time + // list, as that may be expensive and impact the thing we're trying + // to measure. + private static final int EXPECTED_FRAME_COUNT = 2048; + + private static boolean mRecordingFrames; + private static List mFrameTimes; + private static long mFrameStartTime; + + private static void initialiseRecordingArrays() { + if (mFrameTimes == null) { + mFrameTimes = new ArrayList(EXPECTED_FRAME_COUNT); + } else { + mFrameTimes.clear(); + } + } + + @RobocopTarget + public static void startFrameTimeRecording() { + if (mRecordingFrames) { + Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!"); + return; + } + mRecordingFrames = true; + initialiseRecordingArrays(); + mFrameStartTime = SystemClock.uptimeMillis(); + } + + @RobocopTarget + public static List stopFrameTimeRecording() { + if (!mRecordingFrames) { + Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!"); + return null; + } + mRecordingFrames = false; + return mFrameTimes; + } + + public static void recordFrameTime() { + // this will be called often, so try to make it as quick as possible + if (mRecordingFrames) { + mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime); + } + } + + @RobocopTarget + public static void startCheckerboardRecording() { + throw new UnsupportedOperationException(); + } + + @RobocopTarget + public static List stopCheckerboardRecording() { + throw new UnsupportedOperationException(); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java new file mode 100644 index 0000000000..3244519da1 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java @@ -0,0 +1,77 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import java.util.concurrent.atomic.AtomicInteger; + +public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub { + private static final String LOGTAG = "RemoteSurfaceAllocator"; + + private static RemoteSurfaceAllocator mInstance; + + private final int mAllocatorId; + /// Monotonically increasing counter used to generate unique handles + /// for each SurfaceTexture by combining with mAllocatorId. + private static AtomicInteger sNextHandle = new AtomicInteger(1); + + /** + * Retrieves the singleton allocator instance for this process. + * + * @param allocatorId A unique ID identifying the process this instance belongs to, which must be + * 0 for the parent process instance. + */ + public static synchronized RemoteSurfaceAllocator getInstance(final int allocatorId) { + if (mInstance == null) { + mInstance = new RemoteSurfaceAllocator(allocatorId); + } + return mInstance; + } + + private RemoteSurfaceAllocator(final int allocatorId) { + mAllocatorId = allocatorId; + } + + @Override + public GeckoSurface acquireSurface( + final int width, final int height, final boolean singleBufferMode) { + final long handle = ((long) mAllocatorId << 32) | sNextHandle.getAndIncrement(); + final GeckoSurfaceTexture gst = GeckoSurfaceTexture.acquire(singleBufferMode, handle); + + if (gst == null) { + return null; + } + + if (width > 0 && height > 0) { + gst.setDefaultBufferSize(width, height); + } + + return new GeckoSurface(gst); + } + + @Override + public void releaseSurface(final long handle) { + final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle); + if (gst != null) { + gst.decrementUse(); + } + } + + @Override + public void configureSync(final SyncConfig config) { + final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(config.sourceTextureHandle); + if (gst != null) { + gst.configureSnapshot(config.targetSurface, config.width, config.height); + } + } + + @Override + public void sync(final long handle) { + final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle); + if (gst != null) { + gst.takeSnapshot(); + } + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java new file mode 100644 index 0000000000..89fba6c2f9 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java @@ -0,0 +1,143 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.LongSparseArray; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.process.GeckoProcessManager; +import org.mozilla.gecko.process.GeckoServiceChildProcess; + +/* package */ final class SurfaceAllocator { + private static final String LOGTAG = "SurfaceAllocator"; + + private static ISurfaceAllocator sAllocator; + + // Keep a reference to all allocated Surfaces, so that we can release them if we lose the + // connection to the allocator service. + private static final LongSparseArray sSurfaces = + new LongSparseArray(); + + private static synchronized void ensureConnection() { + if (sAllocator != null) { + return; + } + + try { + if (GeckoAppShell.isParentProcess()) { + sAllocator = GeckoProcessManager.getInstance().getSurfaceAllocator(); + } else { + sAllocator = GeckoServiceChildProcess.getSurfaceAllocator(); + } + + if (sAllocator == null) { + Log.w(LOGTAG, "Failed to connect to RemoteSurfaceAllocator"); + return; + } + sAllocator + .asBinder() + .linkToDeath( + new IBinder.DeathRecipient() { + @Override + public void binderDied() { + Log.w(LOGTAG, "RemoteSurfaceAllocator died"); + synchronized (SurfaceAllocator.class) { + // Our connection to the remote allocator has died, so all our surfaces are + // invalid. Release them all now. When their owners attempt to render in to + // them they can detect they have been released and allocate new ones instead. + for (int i = 0; i < sSurfaces.size(); i++) { + sSurfaces.valueAt(i).release(); + } + sSurfaces.clear(); + sAllocator = null; + } + } + }, + 0); + } catch (final RemoteException e) { + Log.w(LOGTAG, "Failed to connect to RemoteSurfaceAllocator", e); + sAllocator = null; + } + } + + @WrapForJNI + public static synchronized GeckoSurface acquireSurface( + final int width, final int height, final boolean singleBufferMode) { + try { + ensureConnection(); + + if (sAllocator == null) { + Log.w(LOGTAG, "Failed to acquire GeckoSurface: not connected"); + return null; + } + + if (singleBufferMode && !GeckoSurfaceTexture.isSingleBufferSupported()) { + return null; + } + + final GeckoSurface surface = sAllocator.acquireSurface(width, height, singleBufferMode); + if (surface == null) { + Log.w(LOGTAG, "Failed to acquire GeckoSurface: RemoteSurfaceAllocator returned null"); + return null; + } + sSurfaces.put(surface.getHandle(), surface); + + if (!surface.inProcess()) { + final SyncConfig config = surface.initSyncSurface(width, height); + if (config != null) { + sAllocator.configureSync(config); + } + } + return surface; + } catch (final RemoteException e) { + Log.w(LOGTAG, "Failed to acquire GeckoSurface", e); + return null; + } + } + + @WrapForJNI + public static synchronized void disposeSurface(final GeckoSurface surface) { + // If the surface has already been released (probably due to losing connection to the remote + // allocator) then there is nothing to do here. + if (surface.isReleased()) { + return; + } + + sSurfaces.remove(surface.getHandle()); + + // Release our Surface + surface.release(); + + if (sAllocator == null) { + return; + } + + // Release the SurfaceTexture on the other side. If we have lost connection then do nothing, as + // there is nothing on the other side to release. + try { + if (sAllocator != null) { + sAllocator.releaseSurface(surface.getHandle()); + } + } catch (final RemoteException e) { + Log.w(LOGTAG, "Failed to release surface texture", e); + } + } + + public static synchronized void sync(final long upstream) { + // Sync from the SurfaceTexture on the other side. If we have lost connection then do nothing, + // as there is nothing on the other side to sync from. + try { + if (sAllocator != null) { + sAllocator.sync(upstream); + } + } catch (final RemoteException e) { + Log.w(LOGTAG, "Failed to sync texture", e); + } + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java new file mode 100644 index 0000000000..e02ab98952 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java @@ -0,0 +1,105 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.Build; +import android.view.Surface; +import android.view.SurfaceControl; +import androidx.annotation.RequiresApi; +import java.util.Iterator; +import java.util.Map; +import java.util.WeakHashMap; +import org.mozilla.gecko.annotation.WrapForJNI; + +// A helper class that creates Surfaces from SurfaceControl objects, for the widget to render in to. +// Unlike the Surfaces provided to the widget directly from the application, these are suitable for +// use in the GPU process as well as the main process. +// +// The reason we must not render directly in to the Surface provided by the application from the GPU +// process is because of a bug on Android versions 12 and later: when the GPU process dies the +// Surface is not detached from the dead process' EGL surface, and any subsequent attempts to +// attach another EGL surface to the Surface will fail. +// +// The application is therefore required to provide the SurfaceControl object to a GeckoDisplay +// whenever rendering in to a SurfaceView. The widget will then obtain a Surface from that +// SurfaceControl using getChildSurface(). Internally, this creates another SurfaceControl as a +// child of the provided SurfaceControl, then creates the Surface from that child. If the GPU +// process dies we are able to simply destroy and recreate the child SurfaceControl objects, thereby +// avoiding the bug. +public class SurfaceControlManager { + private static final String LOGTAG = "SurfaceControlManager"; + + private static final SurfaceControlManager sInstance = new SurfaceControlManager(); + + private WeakHashMap mChildSurfaceControls = new WeakHashMap<>(); + + @WrapForJNI + public static SurfaceControlManager getInstance() { + return sInstance; + } + + // Returns a Surface of the requested size that will be composited in to the specified + // SurfaceControl. + @RequiresApi(api = Build.VERSION_CODES.Q) + @WrapForJNI(exceptionMode = "abort") + public synchronized Surface getChildSurface( + final SurfaceControl parent, final int width, final int height) { + SurfaceControl child = mChildSurfaceControls.get(parent); + if (child == null) { + // We must periodically check if any of the SurfaceControls we are managing have been + // destroyed, as we are unable to directly listen to their SurfaceViews' surfaceDestroyed + // callbacks, and they may not be attached to any compositor when they are destroyed meaning + // we cannot perform cleanup in response to the compositor being paused. + // Doing so here, when we encounter a new SurfaceControl instance, is a reasonable guess as to + // when a previous instance may have been released. + final Iterator> it = + mChildSurfaceControls.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry entry = it.next(); + if (!entry.getKey().isValid()) { + it.remove(); + } + } + + child = new SurfaceControl.Builder().setParent(parent).setName("GeckoSurface").build(); + mChildSurfaceControls.put(parent, child); + } + + new SurfaceControl.Transaction() + .setVisibility(child, true) + .setBufferSize(child, width, height) + .apply(); + + return new Surface(child); + } + + // Removes an existing parent SurfaceControl and its corresponding child from the manager. This + // can be used when we require the next call to getChildSurface() for the specified parent to + // create a new child rather than return the existing one. + @RequiresApi(api = Build.VERSION_CODES.Q) + @WrapForJNI(exceptionMode = "abort") + public synchronized void removeSurface(final SurfaceControl parent) { + final SurfaceControl child = mChildSurfaceControls.remove(parent); + if (child != null) { + child.release(); + } + } + + // Must be called whenever the GPU process has died. This destroys all the child SurfaceControls + // that have been created, meaning subsequent calls to getChildSurface() will create new ones. + @RequiresApi(api = Build.VERSION_CODES.Q) + @WrapForJNI(exceptionMode = "abort") + public synchronized void onGpuProcessLoss() { + for (final SurfaceControl child : mChildSurfaceControls.values()) { + // We could reparent the child SurfaceControl to null here to immediately remove it from the + // tree. However, this will result in a black screen while we wait for the new compositor to + // be created. It's preferable for the user to see the old content instead, so simply call + // release(). + child.release(); + } + mChildSurfaceControls.clear(); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java new file mode 100644 index 0000000000..0ba79d1f42 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java @@ -0,0 +1,38 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.SurfaceTexture; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.mozglue.JNIObject; + +/* package */ final class SurfaceTextureListener extends JNIObject + implements SurfaceTexture.OnFrameAvailableListener { + @WrapForJNI(calledFrom = "gecko") + private SurfaceTextureListener() {} + + @WrapForJNI(dispatchTo = "gecko") + @Override // JNIObject + protected native void disposeNative(); + + @Override + protected void finalize() { + disposeNative(); + } + + @WrapForJNI(stubName = "OnFrameAvailable") + private native void nativeOnFrameAvailable(); + + @Override // SurfaceTexture.OnFrameAvailableListener + public void onFrameAvailable(final SurfaceTexture surfaceTexture) { + try { + nativeOnFrameAvailable(); + } catch (final NullPointerException e) { + // Ignore exceptions caused by a disposed object, i.e. + // getting a callback after this listener is no longer in use. + } + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java new file mode 100644 index 0000000000..d8e2099ddc --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.gfx; + +import android.os.Parcel; +import android.os.Parcelable; + +/* package */ final class SyncConfig implements Parcelable { + final long sourceTextureHandle; + final GeckoSurface targetSurface; + final int width; + final int height; + + /* package */ SyncConfig( + final long sourceTextureHandle, + final GeckoSurface targetSurface, + final int width, + final int height) { + this.sourceTextureHandle = sourceTextureHandle; + this.targetSurface = targetSurface; + this.width = width; + this.height = height; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public SyncConfig createFromParcel(final Parcel parcel) { + return new SyncConfig(parcel); + } + + @Override + public SyncConfig[] newArray(final int i) { + return new SyncConfig[i]; + } + }; + + private SyncConfig(final Parcel parcel) { + sourceTextureHandle = parcel.readLong(); + targetSurface = GeckoSurface.CREATOR.createFromParcel(parcel); + width = parcel.readInt(); + height = parcel.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel parcel, final int flags) { + parcel.writeLong(sourceTextureHandle); + targetSurface.writeToParcel(parcel, flags); + parcel.writeInt(width); + parcel.writeInt(height); + } +} -- cgit v1.2.3