blob: 0e011bb0d0b3e745b7e60345ffacb5ba02a2613c [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.window;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.os.Handler;
import android.os.Looper;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceControl.Transaction;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewRootImpl;
import com.android.internal.annotations.GuardedBy;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Used to organize syncs for surfaces.
*
* The SurfaceSyncer allows callers to add desired syncs into a set and wait for them to all
* complete before getting a callback. The purpose of the Syncer is to be an accounting mechanism
* so each sync implementation doesn't need to handle it themselves. The Syncer class is used the
* following way.
*
* 1. {@link #setupSync(Runnable)} is called
* 2. {@link #addToSync(int, SyncTarget)} is called for every SyncTarget object that wants to be
* included in the sync. If the addSync is called for a View or SurfaceView it needs to be called
* on the UI thread. When addToSync is called, it's guaranteed that any UI updates that were
* requested before addToSync but after the last frame drew, will be included in the sync.
* 3. {@link #markSyncReady(int)} should be called when all the {@link SyncTarget}s have been added
* to the SyncSet. Now the SyncSet is closed and no more SyncTargets can be added to it.
* 4. The SyncSet will gather the data for each SyncTarget using the steps described below. When
* all the SyncTargets have finished, the syncRequestComplete will be invoked and the transaction
* will either be applied or sent to the caller. In most cases, only the SurfaceSyncer should be
* handling the Transaction object directly. However, there are some cases where the framework
* needs to send the Transaction elsewhere, like in ViewRootImpl, so that option is provided.
*
* The following is what happens within the {@link SyncSet}
* 1. Each SyncableTarget will get a {@link SyncTarget#onReadyToSync} callback that contains
* a {@link SyncBufferCallback}.
* 2. Each {@link SyncTarget} needs to invoke {@link SyncBufferCallback#onBufferReady(Transaction)}.
* This makes sure the SyncSet knows when the SyncTarget is complete, allowing the SyncSet to get
* the Transaction that contains the buffer.
* 3. When the final SyncBufferCallback finishes for the SyncSet, the syncRequestComplete Consumer
* will be invoked with the transaction that contains all information requested in the sync. This
* could include buffers and geometry changes. The buffer update will include the UI changes that
* were requested for the View.
*
* @hide
*/
public class SurfaceSyncer {
private static final String TAG = "SurfaceSyncer";
private static final boolean DEBUG = false;
private static Supplier<Transaction> sTransactionFactory = Transaction::new;
private final Object mSyncSetLock = new Object();
@GuardedBy("mSyncSetLock")
private final SparseArray<SyncSet> mSyncSets = new SparseArray<>();
@GuardedBy("mSyncSetLock")
private int mIdCounter = 0;
/**
* @hide
*/
public static void setTransactionFactory(Supplier<Transaction> transactionFactory) {
sTransactionFactory = transactionFactory;
}
/**
* Starts a sync and will automatically apply the final, merged transaction.
*
* @param onComplete The runnable that is invoked when the sync has completed. This will run on
* the same thread that the sync was started on.
* @return The syncId for the newly created sync.
* @see #setupSync(Consumer)
*/
public int setupSync(@Nullable Runnable onComplete) {
Handler handler = new Handler(Looper.myLooper());
return setupSync(transaction -> {
transaction.apply();
if (onComplete != null) {
handler.post(onComplete);
}
});
}
/**
* Starts a sync.
*
* @param syncRequestComplete The complete callback that contains the syncId and transaction
* with all the sync data merged.
* @return The syncId for the newly created sync.
* @hide
* @see #setupSync(Runnable)
*/
public int setupSync(@NonNull Consumer<Transaction> syncRequestComplete) {
synchronized (mSyncSetLock) {
mIdCounter++;
if (DEBUG) {
Log.d(TAG, "setupSync " + mIdCounter);
}
SyncSet syncSet = new SyncSet(mIdCounter, syncRequestComplete);
mSyncSets.put(mIdCounter, syncSet);
return mIdCounter;
}
}
/**
* Mark the sync set as ready to complete. No more data can be added to the specified syncId.
* Once the sync set is marked as ready, it will be able to complete once all Syncables in the
* set have completed their sync
*
* @param syncId The syncId to mark as ready.
*/
public void markSyncReady(int syncId) {
SyncSet syncSet;
synchronized (mSyncSetLock) {
syncSet = mSyncSets.get(syncId);
mSyncSets.remove(syncId);
}
if (syncSet == null) {
Log.e(TAG, "Failed to find syncSet for syncId=" + syncId);
return;
}
syncSet.markSyncReady();
}
/**
* Add a SurfaceView to a sync set. This is different than {@link #addToSync(int, View)} because
* it requires the caller to notify the start and finish drawing in order to sync.
*
* @param syncId The syncId to add an entry to.
* @param surfaceView The SurfaceView to add to the sync.
* @param frameCallbackConsumer The callback that's invoked to allow the caller to notify the
* Syncer when the SurfaceView has started drawing and finished.
*
* @return true if the SurfaceView was successfully added to the SyncSet, false otherwise.
*/
@UiThread
public boolean addToSync(int syncId, SurfaceView surfaceView,
Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) {
return addToSync(syncId, new SurfaceViewSyncTarget(surfaceView, frameCallbackConsumer));
}
/**
* Add a View's rootView to a sync set.
*
* @param syncId The syncId to add an entry to.
* @param view The view where the root will be add to the sync set
*
* @return true if the View was successfully added to the SyncSet, false otherwise.
*/
@UiThread
public boolean addToSync(int syncId, @NonNull View view) {
ViewRootImpl viewRoot = view.getViewRootImpl();
if (viewRoot == null) {
return false;
}
return addToSync(syncId, viewRoot.mSyncTarget);
}
/**
* Add a {@link SyncTarget} to a sync set. The sync set will wait for all
* SyncableSurfaces to complete before notifying.
*
* @param syncId The syncId to add an entry to.
* @param syncTarget A SyncableSurface that implements how to handle syncing
* buffers.
*
* @return true if the SyncTarget was successfully added to the SyncSet, false otherwise.
*/
public boolean addToSync(int syncId, @NonNull SyncTarget syncTarget) {
SyncSet syncSet = getAndValidateSyncSet(syncId);
if (syncSet == null) {
return false;
}
if (DEBUG) {
Log.d(TAG, "addToSync id=" + syncId);
}
syncSet.addSyncableSurface(syncTarget);
return true;
}
/**
* Add a transaction to a specific sync so it can be merged along with the frames from the
* Syncables in the set. This is so the caller can add arbitrary transaction info that will be
* applied at the same time as the buffers
* @param syncId The syncId where the transaction will be merged to.
* @param t The transaction to merge in the sync set.
*/
public void addTransactionToSync(int syncId, Transaction t) {
SyncSet syncSet = getAndValidateSyncSet(syncId);
if (syncSet != null) {
syncSet.addTransactionToSync(t);
}
}
private SyncSet getAndValidateSyncSet(int syncId) {
SyncSet syncSet;
synchronized (mSyncSetLock) {
syncSet = mSyncSets.get(syncId);
}
if (syncSet == null) {
Log.e(TAG, "Failed to find sync for id=" + syncId);
return null;
}
return syncSet;
}
/**
* A SyncTarget that can be added to a sync set.
*/
public interface SyncTarget {
/**
* Called when the Syncable is ready to begin handing a sync request. When invoked, the
* implementor is required to call {@link SyncBufferCallback#onBufferReady(Transaction)}
* and {@link SyncBufferCallback#onBufferReady(Transaction)} in order for this Syncable
* to be marked as complete.
*
* Always invoked on the thread that initiated the call to
* {@link #addToSync(int, SyncTarget)}
*
* @param syncBufferCallback A SyncBufferCallback that the caller must invoke onBufferReady
*/
void onReadyToSync(SyncBufferCallback syncBufferCallback);
/**
* There's no guarantee about the thread this callback is invoked on.
*/
default void onSyncComplete() {}
}
/**
* Interface so the SurfaceSyncer can know when it's safe to start and when everything has been
* completed. The caller should invoke the calls when the rendering has started and finished a
* frame.
*/
public interface SyncBufferCallback {
/**
* Invoked when the transaction contains the buffer and is ready to sync.
*
* @param t The transaction that contains the buffer to be synced. This can be null if
* there's nothing to sync
*/
void onBufferReady(@Nullable Transaction t);
}
/**
* Class that collects the {@link SyncTarget}s and notifies when all the surfaces have
* a frame ready.
*/
private static class SyncSet {
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Set<Integer> mPendingSyncs = new ArraySet<>();
@GuardedBy("mLock")
private final Transaction mTransaction = sTransactionFactory.get();
@GuardedBy("mLock")
private boolean mSyncReady;
@GuardedBy("mLock")
private final Set<SyncTarget> mSyncTargets = new ArraySet<>();
private final int mSyncId;
private final Consumer<Transaction> mSyncRequestCompleteCallback;
private SyncSet(int syncId, Consumer<Transaction> syncRequestComplete) {
mSyncId = syncId;
mSyncRequestCompleteCallback = syncRequestComplete;
}
void addSyncableSurface(SyncTarget syncTarget) {
SyncBufferCallback syncBufferCallback = new SyncBufferCallback() {
@Override
public void onBufferReady(Transaction t) {
synchronized (mLock) {
if (t != null) {
mTransaction.merge(t);
}
mPendingSyncs.remove(hashCode());
checkIfSyncIsComplete();
}
}
};
synchronized (mLock) {
mPendingSyncs.add(syncBufferCallback.hashCode());
mSyncTargets.add(syncTarget);
}
syncTarget.onReadyToSync(syncBufferCallback);
}
void markSyncReady() {
synchronized (mLock) {
mSyncReady = true;
checkIfSyncIsComplete();
}
}
@GuardedBy("mLock")
private void checkIfSyncIsComplete() {
if (!mSyncReady || !mPendingSyncs.isEmpty()) {
if (DEBUG) {
Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady
+ " mPendingSyncs=" + mPendingSyncs.size());
}
return;
}
if (DEBUG) {
Log.d(TAG, "Successfully finished sync id=" + mSyncId);
}
for (SyncTarget syncTarget : mSyncTargets) {
syncTarget.onSyncComplete();
}
mSyncTargets.clear();
mSyncRequestCompleteCallback.accept(mTransaction);
}
/**
* Add a Transaction to this sync set. This allows the caller to provide other info that
* should be synced with the buffers.
*/
void addTransactionToSync(Transaction t) {
synchronized (mLock) {
mTransaction.merge(t);
}
}
}
/**
* Wrapper class to help synchronize SurfaceViews
*/
private static class SurfaceViewSyncTarget implements SyncTarget {
private final SurfaceView mSurfaceView;
private final Consumer<SurfaceViewFrameCallback> mFrameCallbackConsumer;
SurfaceViewSyncTarget(SurfaceView surfaceView,
Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) {
mSurfaceView = surfaceView;
mFrameCallbackConsumer = frameCallbackConsumer;
}
@Override
public void onReadyToSync(SyncBufferCallback syncBufferCallback) {
mFrameCallbackConsumer.accept(
() -> mSurfaceView.syncNextFrame(syncBufferCallback::onBufferReady));
}
}
/**
* A frame callback that is used to synchronize SurfaceViews. The owner of the SurfaceView must
* implement onFrameStarted when trying to sync the SurfaceView. This is to ensure the sync
* knows when the frame is ready to add to the sync.
*/
public interface SurfaceViewFrameCallback {
/**
* Called when the SurfaceView is going to render a frame
*/
void onFrameStarted();
}
}