Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 747b5bfa authored by Chavi Weingarten's avatar Chavi Weingarten Committed by Android (Google) Code Review
Browse files

Merge "Add SurfaceSyncer class" into tm-dev

parents 7e8d4446 d32068af
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -1789,4 +1789,11 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
            t.apply();
        }
    }

    /**
     * @hide
     */
    public void syncNextFrame(Transaction t) {
        mBlastBufferQueue.setSyncTransaction(t);
    }
}
+68 −0
Original line number Diff line number Diff line
@@ -195,6 +195,7 @@ import android.view.contentcapture.MainContentCaptureSession;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
import android.window.ClientWindowFrames;
import android.window.SurfaceSyncer;
import android.window.WindowOnBackInvokedDispatcher;

import com.android.internal.R;
@@ -10873,4 +10874,71 @@ public final class ViewRootImpl implements ViewParent,
    IWindowSession getWindowSession() {
        return mWindowSession;
    }

    private void registerCallbacksForSync(
            final SurfaceSyncer.SyncBufferCallback syncBufferCallback) {
        if (!isHardwareEnabled()) {
            // TODO: correctly handle when hardware disabled
            syncBufferCallback.onBufferReady(null);
            return;
        }

        mAttachInfo.mThreadedRenderer.registerRtFrameCallback(new FrameDrawingCallback() {
            @Override
            public void onFrameDraw(long frame) {
            }

            @Override
            public HardwareRenderer.FrameCommitCallback onFrameDraw(int syncResult, long frame) {
                if (DEBUG_BLAST) {
                    Log.d(mTag,
                            "Received frameDrawingCallback syncResult=" + syncResult + " frameNum="
                                    + frame + ".");
                }

                final Transaction t = new Transaction();

                // If the syncResults are SYNC_LOST_SURFACE_REWARD_IF_FOUND or
                // SYNC_CONTEXT_IS_STOPPED it means nothing will draw. There's no need to set up
                // any blast sync or commit callback, and the code should directly call
                // pendingDrawFinished.
                if ((syncResult
                        & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
                    t.merge(mBlastBufferQueue.gatherPendingTransactions(frame));
                    syncBufferCallback.onBufferReady(t);
                    return null;
                }

                mBlastBufferQueue.setSyncTransaction(t);
                if (DEBUG_BLAST) {
                    Log.d(mTag, "Setting up sync and frameCommitCallback");
                }

                return didProduceBuffer -> {
                    if (DEBUG_BLAST) {
                        Log.d(mTag, "Received frameCommittedCallback"
                                + " lastAttemptedDrawFrameNum=" + frame
                                + " didProduceBuffer=" + didProduceBuffer);
                    }

                    // If frame wasn't drawn, clear out the next transaction so it doesn't affect
                    // the next draw attempt. The next transaction and transaction complete callback
                    // were only set for the current draw attempt.
                    if (!didProduceBuffer) {
                        mBlastBufferQueue.setSyncTransaction(null);
                        // Gather the transactions that were sent to mergeWithNextTransaction
                        // since the frame didn't draw on this vsync. It's possible the frame will
                        // draw later, but it's better to not be sync than to block on a frame that
                        // may never come.
                        t.merge(mBlastBufferQueue.gatherPendingTransactions(frame));
                    }

                    syncBufferCallback.onBufferReady(t);
                };
            }
        });
    }

    public final SurfaceSyncer.SyncTarget mSyncTarget =
            syncBufferCallback -> registerCallbacksForSync(syncBufferCallback);
}
+377 −0
Original line number Diff line number Diff line
/*
 * 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.Log;
import android.util.SparseArray;
import android.view.SurfaceControl.Transaction;
import android.view.SurfaceView;
import android.view.View;

import com.android.internal.annotations.GuardedBy;

import java.util.HashSet;
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();
            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) {
        return addToSync(syncId, view.getViewRootImpl().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.
         *
         * @param syncBufferCallback A SyncBufferCallback that the caller must invoke onBufferReady
         */
        void onReadyToSync(SyncBufferCallback syncBufferCallback);
    }

    /**
     * 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 HashSet<>();
        @GuardedBy("mLock")
        private final Transaction mTransaction = sTransactionFactory.get();
        @GuardedBy("mLock")
        private boolean mSyncReady;

        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());
            }
            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);
            }
            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) {
            Transaction mTransaction = sTransactionFactory.get();
            mFrameCallbackConsumer.accept(new SurfaceViewFrameCallback() {
                @Override
                public void onFrameStarted() {
                    mSurfaceView.syncNextFrame(mTransaction);
                }

                @Override
                public void onFrameComplete() {
                    syncBufferCallback.onBufferReady(mTransaction);
                }
            });
        }
    }

    /**
     * A frame callback that is used to synchronize SurfaceViews. The owner of the SurfaceView must
     * implement onFrameStarted and onFrameComplete 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();

        /**
         * Called when the SurfaceView has finished rendering a frame.
         */
        void onFrameComplete();
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ android_test {
        "ub-uiautomator",
        "hamcrest-library",
        "platform-compat-test-rules",
        "CtsSurfaceValidatorLib",
    ],

    libs: [
+8 −0
Original line number Diff line number Diff line
@@ -43,6 +43,8 @@
    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
    <uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT"/>
    <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
    <application android:debuggable="true"
@@ -78,6 +80,12 @@
                  android:turnScreenOn="true"
                  android:showWhenLocked="true" />
        <activity android:name="com.android.server.wm.ScreenshotTests$ScreenshotActivity" />
        <activity android:name="android.view.cts.surfacevalidator.CapturedActivity"/>

        <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
            android:foregroundServiceType="mediaProjection"
            android:enabled="true">
        </service>
    </application>

    <instrumentation
Loading