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

Commit a3f13550 authored by Melody Hsu's avatar Melody Hsu
Browse files

Recover from buffer stuffing for canned animations

Buffer stuffing occurs when SurfaceFlinger misses a frame, but the
client continues to produce buffers at the same rate, causing a
greater risk for jank to occur. Recovery is achieved for canned
animations by adjusting the animation timeline on the client side so
that SurfaceFlinger is no longer behind.

Choreographer adds a negative offset to each frame when buffer
stuffing is detected to effectively adjust the animation timeline
after an intentional frame delay.

Bug: b/294922229
Test: presubmit, manually check perfetto traces
Flag: android.view.flags.buffer_stuffing_recovery
Change-Id: Ic48208252c899a97cd72177fedaa2c2788028338
parent 2f6d590b
Loading
Loading
Loading
Loading
+154 −3
Original line number Diff line number Diff line
@@ -189,6 +189,11 @@ public final class Choreographer {
    @UnsupportedAppUsage
    private long mLastFrameTimeNanos;

    // Keeps track of the last scheduled frame time without additional offsets
    // added from buffer stuffing recovery. Used to compare timing of vsyncs to
    // determine idle state.
    private long mLastNoOffsetFrameTimeNanos;

    /** DO NOT USE since this will not updated when screen refresh changes. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R,
            publicAlternatives = "Use {@link android.view.Display#getRefreshRate} instead")
@@ -203,6 +208,50 @@ public final class Choreographer {
    private final FrameData mFrameData = new FrameData();
    private volatile boolean mInDoFrameCallback = false;

    private static class BufferStuffingData {
        enum RecoveryAction {
            // No recovery
            NONE,
            // Recovery has started, adds a negative offset
            OFFSET,
            // Recovery has started, delays a frame to return buffer count
            // back toward threshold.
            DELAY_FRAME
        }
        // The maximum number of times frames will be delayed per buffer stuffing event.
        // Since buffer stuffing can persist for several consecutive frames following the
        // initial missed frame, we want to adjust the timeline with enough frame delays and
        // offsets to return the queued buffer count back to threshold.
        public static final int MAX_FRAME_DELAYS = 3;

        // Whether buffer stuffing recovery has begun. Recovery can only end
        // when events are idle.
        public boolean isRecovering = false;

        // The number of frames delayed so far during recovery. Used to compare with
        // MAX_FRAME_DELAYS to safeguard against excessive frame delays during recovery.
        // Also used as unique cookie for tracing.
        public int numberFrameDelays = 0;

        // The number of additional frame delays scheduled during recovery to wait for the next
        // vsync. These are scheduled when frame times appear to go backward or frames are
        // being skipped due to FPSDivisor.
        public int numberWaitsForNextVsync = 0;

        /**
         * After buffer stuffing recovery has ended with a detected idle state, the
         * recovery data trackers can be reset in preparation for any future
         * stuffing events.
         */
        public void reset() {
            isRecovering = false;
            numberFrameDelays = 0;
            numberWaitsForNextVsync = 0;
        }
    }

    private final BufferStuffingData mBufferStuffingData = new BufferStuffingData();

    /**
     * Contains information about the current frame for jank-tracking,
     * mainly timings of key events along with a bit of metadata about
@@ -850,13 +899,99 @@ public final class Choreographer {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    // Conducts logic for beginning or ending buffer stuffing recovery.
    // Returns an enum for the recovery action that should be taken in doFrame().
    BufferStuffingData.RecoveryAction checkBufferStuffingRecovery(long frameTimeNanos,
            DisplayEventReceiver.VsyncEventData vsyncEventData) {
        // Canned animations can recover from buffer stuffing whenever more
        // than 2 buffers are queued.
        if (vsyncEventData.numberQueuedBuffers > 2) {
            mBufferStuffingData.isRecovering = true;
            // Intentional frame delay that can happen at most MAX_FRAME_DELAYS times per
            // buffer stuffing event until the buffer count returns to threshold. The
            // delayed frames are compensated for by the negative offsets added to the
            // animation timestamps.
            if (mBufferStuffingData.numberFrameDelays < mBufferStuffingData.MAX_FRAME_DELAYS) {
                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                    Trace.asyncTraceForTrackBegin(
                            Trace.TRACE_TAG_VIEW, "Buffer stuffing recovery", "Thread "
                            + android.os.Process.myTid() + ", recover frame #"
                            + mBufferStuffingData.numberFrameDelays,
                            mBufferStuffingData.numberFrameDelays);
                }
                mBufferStuffingData.numberFrameDelays++;
                scheduleVsyncLocked();
                return BufferStuffingData.RecoveryAction.DELAY_FRAME;
            }
        }

        if (mBufferStuffingData.isRecovering) {
            // Includes an additional expected frame delay from the natural scheduling
            // of the next vsync event.
            int totalFrameDelays = mBufferStuffingData.numberFrameDelays
                    + mBufferStuffingData.numberWaitsForNextVsync + 1;
            long vsyncsSinceLastCallback =
                    (frameTimeNanos - mLastNoOffsetFrameTimeNanos) / mLastFrameIntervalNanos;

            // Detected idle state due to a longer inactive period since the last vsync callback
            // than the total expected number of vsync frame delays. End buffer stuffing recovery.
            // There are no frames to animate and offsets no longer need to be added
            // since the idle state gives the animation a chance to catch up.
            if (vsyncsSinceLastCallback > totalFrameDelays) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "End buffer stuffing recovery");
                }
                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                    for (int i = 0; i < mBufferStuffingData.numberFrameDelays; i++) {
                        Trace.asyncTraceForTrackEnd(
                                Trace.TRACE_TAG_VIEW, "Buffer stuffing recovery", i);
                    }
                }
                mBufferStuffingData.reset();

            } else {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Adjust animation timeline with a negative offset");
                }
                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                    Trace.instantForTrack(
                            Trace.TRACE_TAG_VIEW, "Buffer stuffing recovery",
                            "Negative offset added to animation");
                }
                return BufferStuffingData.RecoveryAction.OFFSET;
            }
        }
        return BufferStuffingData.RecoveryAction.NONE;
    }

    void doFrame(long frameTimeNanos, int frame,
            DisplayEventReceiver.VsyncEventData vsyncEventData) {
        final long startNanos;
        final long frameIntervalNanos = vsyncEventData.frameInterval;
        boolean resynced = false;
        long offsetFrameTimeNanos = frameTimeNanos;

        // Evaluate if buffer stuffing recovery needs to start or end, and
        // what actions need to be taken for recovery.
        switch (checkBufferStuffingRecovery(frameTimeNanos, vsyncEventData)) {
            case NONE:
                // Without buffer stuffing recovery, offsetFrameTimeNanos is
                // synonymous with frameTimeNanos.
                break;
            case OFFSET:
                // Add animation offset. Used to update frame timeline with
                // offset before jitter is calculated.
                offsetFrameTimeNanos = frameTimeNanos - frameIntervalNanos;
                break;
            case DELAY_FRAME:
                // Intentional frame delay to help restore queued buffer count to threshold.
                return;
            default:
                break;
        }

        try {
            FrameTimeline timeline = mFrameData.update(frameTimeNanos, vsyncEventData);
            FrameTimeline timeline = mFrameData.update(offsetFrameTimeNanos, vsyncEventData);
            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                Trace.traceBegin(
                        Trace.TRACE_TAG_VIEW, "Choreographer#doFrame " + timeline.mVsyncId);
@@ -867,15 +1002,18 @@ public final class Choreographer {
                    traceMessage("Frame not scheduled");
                    return; // no work to do
                }
                mLastNoOffsetFrameTimeNanos = frameTimeNanos;

                if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                    mDebugPrintNextFrameTimeDelta = false;
                    Log.d(TAG, "Frame time delta: "
                            + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
                            + ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
                }

                long intendedFrameTimeNanos = frameTimeNanos;
                long intendedFrameTimeNanos = offsetFrameTimeNanos;
                startNanos = System.nanoTime();
                // Calculating jitter involves using the original frame time without
                // adjustments from buffer stuffing
                final long jitterNanos = startNanos - frameTimeNanos;
                if (jitterNanos >= frameIntervalNanos) {
                    frameTimeNanos = startNanos;
@@ -899,6 +1037,13 @@ public final class Choreographer {
                                    + " ms in the past.");
                        }
                    }
                    if (mBufferStuffingData.isRecovering) {
                        frameTimeNanos -= frameIntervalNanos;
                        if (DEBUG_JANK) {
                            Log.d(TAG, "Adjusted animation timeline with a negative offset after"
                                    + " jitter calculation");
                        }
                    }
                    timeline = mFrameData.update(
                            frameTimeNanos, mDisplayEventReceiver, jitterNanos);
                    resynced = true;
@@ -910,6 +1055,9 @@ public final class Choreographer {
                                + "previously skipped frame.  Waiting for next vsync.");
                    }
                    traceMessage("Frame time goes backward");
                    if (mBufferStuffingData.isRecovering) {
                        mBufferStuffingData.numberWaitsForNextVsync++;
                    }
                    scheduleVsyncLocked();
                    return;
                }
@@ -918,6 +1066,9 @@ public final class Choreographer {
                    long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                    if (timeSinceVsync < (frameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                        traceMessage("Frame skipped due to FPSDivisor");
                        if (mBufferStuffingData.isRecovering) {
                            mBufferStuffingData.numberWaitsForNextVsync++;
                        }
                        scheduleVsyncLocked();
                        return;
                    }
+6 −1
Original line number Diff line number Diff line
@@ -207,6 +207,8 @@ public abstract class DisplayEventReceiver {
        // reasonable timestamps.
        public int frameTimelinesLength = 1;

        public int numberQueuedBuffers = 0;

        VsyncEventData() {
            frameTimelines = new FrameTimeline[FRAME_TIMELINES_CAPACITY];
            for (int i = 0; i < frameTimelines.length; i++) {
@@ -217,11 +219,13 @@ public abstract class DisplayEventReceiver {
        // Called from native code.
        @SuppressWarnings("unused")
        VsyncEventData(FrameTimeline[] frameTimelines, int preferredFrameTimelineIndex,
                int frameTimelinesLength, long frameInterval) {
                int frameTimelinesLength, long frameInterval,
                int numberQueuedBuffers) {
            this.frameTimelines = frameTimelines;
            this.preferredFrameTimelineIndex = preferredFrameTimelineIndex;
            this.frameTimelinesLength = frameTimelinesLength;
            this.frameInterval = frameInterval;
            this.numberQueuedBuffers = numberQueuedBuffers;
        }

        void copyFrom(VsyncEventData other) {
@@ -231,6 +235,7 @@ public abstract class DisplayEventReceiver {
            for (int i = 0; i < frameTimelines.length; i++) {
                frameTimelines[i].copyFrom(other.frameTimelines[i]);
            }
            numberQueuedBuffers = other.numberQueuedBuffers;
        }

        public FrameTimeline preferredFrameTimeline() {
+25 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.graphics.Matrix.MSKEW_X;
import static android.graphics.Matrix.MSKEW_Y;
import static android.graphics.Matrix.MTRANS_X;
import static android.graphics.Matrix.MTRANS_Y;
import static android.view.flags.Flags.bufferStuffingRecovery;
import static android.view.SurfaceControlProto.HASH_CODE;
import static android.view.SurfaceControlProto.LAYER_ID;
import static android.view.SurfaceControlProto.NAME;
@@ -660,6 +661,13 @@ public final class SurfaceControl implements Parcelable {
     */
    public static final int CAN_OCCLUDE_PRESENTATION = 0x00001000;

    /**
     * Indicates that the SurfaceControl should recover from buffer stuffing when
     * possible. This is the case when the SurfaceControl is a ViewRootImpl.
     * @hide
     */
    public static final int RECOVERABLE_FROM_BUFFER_STUFFING = 0x00002000;

    /**
     * Surface creation flag: Creates a surface where color components are interpreted
     * as "non pre-multiplied" by their alpha channel. Of course this flag is
@@ -4813,6 +4821,23 @@ public final class SurfaceControl implements Parcelable {
            nativeSetDesiredPresentTimeNanos(mNativeObject, desiredPresentTimeNanos);
            return this;
        }

        /**
         * Specifies that the SurfaceControl is a buffer producer that should recover from buffer
         * stuffing, meaning that the SurfaceControl is a ViewRootImpl.
         *
         * @hide
         */
        @NonNull
        public Transaction setRecoverableFromBufferStuffing(@NonNull SurfaceControl sc) {
            if (bufferStuffingRecovery()) {
                checkPreconditions(sc);
                nativeSetFlags(mNativeObject, sc.mNativeObject, RECOVERABLE_FROM_BUFFER_STUFFING,
                        RECOVERABLE_FROM_BUFFER_STUFFING);
            }
            return this;
        }

        /**
         * Writes the transaction to parcel, clearing the transaction as if it had been applied so
         * it can be used to store future transactions. It's the responsibility of the parcel
+3 −0
Original line number Diff line number Diff line
@@ -2757,6 +2757,9 @@ public final class ViewRootImpl implements ViewParent,
        // Only call transferFrom if the surface has changed to prevent inc the generation ID and
        // causing EGL resources to be recreated.
        mSurface.transferFrom(blastSurface);
        // Since the SurfaceControl is a VRI, indicate that it can recover from buffer stuffing.
        mTransaction.setRecoverableFromBufferStuffing(mSurfaceControl).applyAsyncUnsafe();
    }
    private void setBoundsLayerCrop(Transaction t) {
+8 −0
Original line number Diff line number Diff line
@@ -133,3 +133,11 @@ flag {
    bug: "333417898"
    is_fixed_read_only: true
}

flag {
    name: "buffer_stuffing_recovery"
    namespace: "window_surfaces"
    description: "Recover from buffer stuffing when SurfaceFlinger misses a frame"
    bug: "294922229"
    is_fixed_read_only: true
}
 No newline at end of file
Loading