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

Commit b8160879 authored by Skuhne's avatar Skuhne
Browse files

Rendering the window frame with a second thread

Using a multi threaded render node to render the window frame
asynchronously from the application relayout.

Bug: 22527834
Bug: 24400680
Bug: 24459827
Bug: 24409773
Bug: 24537510
Change-Id: I1010fc6a8b6e38424178140afa3ca124433ab7e4
parent 3da3ca60
Loading
Loading
Loading
Loading
+5 −5
Original line number Diff line number Diff line
@@ -349,7 +349,7 @@ public class ThreadedRenderer extends HardwareRenderer {
     * @param right The right side of the protected bounds.
     * @param bottom The bottom side of the protected bounds.
     */
    public void setContentOverdrawProtectionBounds(int left, int top, int right, int bottom) {
    public void setContentDrawBounds(int left, int top, int right, int bottom) {
        mStagedContentBounds.set(left, top, right, bottom);
    }

@@ -370,7 +370,7 @@ public class ThreadedRenderer extends HardwareRenderer {
        // renderer.
        if (!mCurrentContentBounds.equals(mStagedContentBounds)) {
            mCurrentContentBounds.set(mStagedContentBounds);
            nSetContentOverdrawProtectionBounds(mNativeProxy, mCurrentContentBounds.left,
            nSetContentDrawBounds(mNativeProxy, mCurrentContentBounds.left,
                    mCurrentContentBounds.top, mCurrentContentBounds.right,
                    mCurrentContentBounds.bottom);
        }
@@ -600,6 +600,6 @@ public class ThreadedRenderer extends HardwareRenderer {
             boolean placeFront);
    private static native void nRemoveRenderNode(long nativeProxy, long rootRenderNode);
    private static native void nDrawRenderNode(long nativeProxy, long rootRenderNode);
    private static native void nSetContentOverdrawProtectionBounds(long nativeProxy, int left,
    private static native void nSetContentDrawBounds(long nativeProxy, int left,
             int top, int right, int bottom);
}
+79 −10
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.WindowCallbacks;
import android.widget.Scroller;

import com.android.internal.R;
@@ -114,6 +115,12 @@ public final class ViewRootImpl implements ViewParent,
    private static final boolean DEBUG_FPS = false;
    private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;

    /**
     * Set to false if we do not want to use the multi threaded renderer. Note that by disabling
     * this, WindowCallbacks will not fire.
     */
    private static final boolean USE_MT_RENDERER = true;

    /**
     * Set this system property to true to force the view hierarchy to render
     * at 60 Hz. This can be used to measure the potential framerate.
@@ -132,11 +139,11 @@ public final class ViewRootImpl implements ViewParent,

    static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();

    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<Runnable>();
    static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList();
    static boolean sFirstDrawComplete = false;
    static final ArrayList<WindowCallbacks> sWindowCallbacks = new ArrayList();

    static final ArrayList<ComponentCallbacks> sConfigCallbacks
            = new ArrayList<ComponentCallbacks>();
    static final ArrayList<ComponentCallbacks> sConfigCallbacks = new ArrayList();

    final Context mContext;
    final IWindowSession mWindowSession;
@@ -417,6 +424,22 @@ public final class ViewRootImpl implements ViewParent,
        }
    }

    public static void addWindowCallbacks(WindowCallbacks callback) {
        if (USE_MT_RENDERER) {
            synchronized (sWindowCallbacks) {
                sWindowCallbacks.add(callback);
            }
        }
    }

    public static void removeWindowCallbacks(WindowCallbacks callback) {
        if (USE_MT_RENDERER) {
            synchronized (sWindowCallbacks) {
                sWindowCallbacks.remove(callback);
            }
        }
    }

    // FIXME for perf testing only
    private boolean mProfile = false;

@@ -1378,6 +1401,7 @@ public final class ViewRootImpl implements ViewParent,
            mAttachInfo.mWindowVisibility = viewVisibility;
            host.dispatchWindowVisibilityChanged(viewVisibility);
            if (viewVisibility != View.VISIBLE || mNewSurfaceNeeded) {
                endDragResizing();
                destroyHardwareResources();
            }
            if (viewVisibility == View.GONE) {
@@ -1701,15 +1725,21 @@ public final class ViewRootImpl implements ViewParent,
                final boolean dragResizing = (relayoutResult
                        & WindowManagerGlobal.RELAYOUT_RES_DRAG_RESIZING) != 0;
                if (mDragResizing != dragResizing) {
                    mDragResizing = dragResizing;
                    mFullRedrawNeeded = true;
                    if (dragResizing) {
                        startDragResizing(frame);
                    } else {
                        // We shouldn't come here, but if we come we should end the resize.
                        endDragResizing();
                    }
                }
                if (!USE_MT_RENDERER) {
                    if (dragResizing) {
                        mCanvasOffsetX = mWinFrame.left;
                        mCanvasOffsetY = mWinFrame.top;
                    } else {
                        mCanvasOffsetX = mCanvasOffsetY = 0;
                    }
                }
            } catch (RemoteException e) {
            }

@@ -6635,6 +6665,15 @@ public final class ViewRootImpl implements ViewParent,
                Configuration newConfig) {
            final ViewRootImpl viewAncestor = mViewAncestor.get();
            if (viewAncestor != null) {
                // Tell all listeners that we are resizing the window so that the chrome can get
                // updated as fast as possible on a separate thread,
                if (mViewAncestor.get().mDragResizing) {
                    synchronized (sWindowCallbacks) {
                        for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
                            sWindowCallbacks.get(i).onWindowSizeIsChanging(frame);
                        }
                    }
                }
                viewAncestor.dispatchResized(frame, overscanInsets, contentInsets,
                        visibleInsets, stableInsets, outsets, reportDraw, newConfig);
            }
@@ -6803,6 +6842,36 @@ public final class ViewRootImpl implements ViewParent,
        return rq;
    }

    /**
     * Start a drag resizing which will inform all listeners that a window resize is taking place.
     */
    private void startDragResizing(Rect initialBounds) {
        if (!mDragResizing) {
            mDragResizing = true;
            synchronized (sWindowCallbacks) {
                for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
                    sWindowCallbacks.get(i).onWindowDragResizeStart(initialBounds);
                }
            }
            mFullRedrawNeeded = true;
        }
    }

    /**
     * End a drag resize which will inform all listeners that a window resize has ended.
     */
    private void endDragResizing() {
        if (mDragResizing) {
            mDragResizing = false;
            synchronized (sWindowCallbacks) {
                for (int i = sWindowCallbacks.size() - 1; i >= 0; i--) {
                    sWindowCallbacks.get(i).onWindowDragResizeEnd();
                }
            }
            mFullRedrawNeeded = true;
        }
    }

    /**
     * Class for managing the accessibility interaction connection
     * based on the global accessibility state.
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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.view;

import android.graphics.Rect;

/**
 * These callbacks are used to communicate window configuration changes while the user is performing
 * window changes.
 * @hide
 */
public interface WindowCallbacks {
    /**
     * Called by the system when the window got changed by the user, before the layouter got called.
     * It can be used to perform a "quick and dirty" resize which should never take more then 4ms to
     * complete.
     *
     * <p>At the time the layouting has not happened yet.
     *
     * @param newBounds The new window frame bounds.
     */
    void onWindowSizeIsChanging(Rect newBounds);

    /**
     * Called when a drag resize starts.
     * @param initialBounds The initial bounds where the window will be.
     */
    void onWindowDragResizeStart(Rect initialBounds);

    /**
     * Called when a drag resize ends.
     */
    void onWindowDragResizeEnd();
}
+1 −1
Original line number Diff line number Diff line
@@ -2615,7 +2615,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
            if (action == MotionEvent.ACTION_DOWN) {
                int y = (int)event.getY();
                if (y >= (getHeight()-5) && !mWindow.hasChildren()) {
                    Log.i(TAG, "Watchiing!");
                    Log.i(TAG, "Watching!");
                    mWatchingForMenu = true;
                }
                return false;
+295 −2
Original line number Diff line number Diff line
@@ -17,15 +17,23 @@
package com.android.internal.widget;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Looper;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.DisplayListCanvas;
import android.view.MotionEvent;
import android.view.RenderNode;
import android.view.ThreadedRenderer;
import android.view.View;
import android.view.ViewRootImpl;
import android.widget.LinearLayout;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.Window;
import android.view.WindowCallbacks;
import android.util.Log;
import android.util.TypedValue;

@@ -58,7 +66,7 @@ import com.android.internal.policy.PhoneWindow;
 * This will be mitigated once b/22527834 will be addressed.
 */
public class NonClientDecorView extends LinearLayout
        implements View.OnClickListener, View.OnTouchListener {
        implements View.OnClickListener, View.OnTouchListener, WindowCallbacks {
    private final static String TAG = "NonClientDecorView";
    // The height of a window which has focus in DIP.
    private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
@@ -67,6 +75,8 @@ public class NonClientDecorView extends LinearLayout
    private PhoneWindow mOwner = null;
    private boolean mWindowHasShadow = false;
    private boolean mShowDecor = false;
    // True when this object is listening for window size changes.
    private boolean mAttachedCallbacksToRootViewImpl = false;

    // True if the window is being dragged.
    private boolean mDragging = false;
@@ -85,6 +95,9 @@ public class NonClientDecorView extends LinearLayout
    // to max until the first layout command has been executed.
    private boolean mAllowUpdateElevation = false;

    // The resize frame renderer.
    private ResizeFrameThread mFrameRendererThread = null;

    public NonClientDecorView(Context context) {
        super(context);
    }
@@ -108,6 +121,18 @@ public class NonClientDecorView extends LinearLayout
        // By changing the outline provider to BOUNDS, the window can remove its
        // background without removing the shadow.
        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);

        if (!mAttachedCallbacksToRootViewImpl) {
            // If there is no window callback installed there was no window set before. Set it now.
            // Note that our ViewRootImpl object will not change.
            getViewRootImpl().addWindowCallbacks(this);
            mAttachedCallbacksToRootViewImpl = true;
        } else if (mFrameRendererThread != null) {
            // We are resizing and this call happened due to a configuration change. Tell the
            // renderer about it.
            mFrameRendererThread.onConfigurationChange();
        }

        findViewById(R.id.maximize_window).setOnClickListener(this);
        findViewById(R.id.close_window).setOnClickListener(this);
    }
@@ -251,7 +276,9 @@ public class NonClientDecorView extends LinearLayout
     **/
    private void updateElevation() {
        float elevation = 0;
        if (mWindowHasShadow) {
        // Do not use a shadow when we are in resizing mode (mRenderer not null) since the shadow
        // is bound to the content size and not the target size.
        if (mWindowHasShadow && mFrameRendererThread == null) {
            boolean fill = isFillingScreen();
            elevation = fill ? 0 :
                    (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
@@ -293,4 +320,270 @@ public class NonClientDecorView extends LinearLayout
            }
        }
    }

    @Override
    public void onWindowDragResizeStart(Rect initialBounds) {
        if (mOwner.isDestroyed()) {
            // If the owner's window is gone, we should not be able to come here anymore.
            releaseResources();
            return;
        }
        if (mFrameRendererThread != null) {
            return;
        }
        final ThreadedRenderer renderer =
                (ThreadedRenderer) mOwner.getDecorView().getHardwareRenderer();
        if (renderer != null) {
            mFrameRendererThread = new ResizeFrameThread(renderer, initialBounds);
            // Get rid of the shadow while we are resizing. Shadow drawing takes considerable time.
            // If we want to get the shadow shown while resizing, we would need to elevate a new
            // element which owns the caption and has the elevation.
            updateElevation();
        }
    }

    @Override
    public void onWindowDragResizeEnd() {
        releaseThreadedRenderer();
    }

    @Override
    public void onWindowSizeIsChanging(Rect newBounds) {
        if (mFrameRendererThread != null) {
            mFrameRendererThread.setTargetRect(newBounds);
        }
    }

    /**
     * Release the renderer thread which is usually done when the user stops resizing.
     */
    private void releaseThreadedRenderer() {
        if (mFrameRendererThread != null) {
            mFrameRendererThread.releaseRenderer();
            mFrameRendererThread = null;
            // Bring the shadow back.
            updateElevation();
        }
    }

    /**
     * Called when the parent window is destroyed to release all resources. Note that this will also
     * destroy the renderer thread.
     */
    private void releaseResources() {
        releaseThreadedRenderer();
        if (mAttachedCallbacksToRootViewImpl) {
            ViewRootImpl.removeWindowCallbacks(this);
            mAttachedCallbacksToRootViewImpl = false;
        }
    }

    /**
     * The thread which draws the chrome while we are resizing.
     * It starts with the creation and it ends once someone calls destroy().
     * Any size changes can be passed by a call to setTargetRect will passed to the thread and
     * executed via the Choreographer.
     */
    private class ResizeFrameThread extends Thread implements Choreographer.FrameCallback {
        // This is containing the last requested size by a resize command. Note that this size might
        // or might not have been applied to the output already.
        private final Rect mTargetRect = new Rect();

        // The render nodes for the multi threaded renderer.
        private ThreadedRenderer mRenderer;
        private RenderNode mFrameNode;
        private RenderNode mBackdropNode;

        private final Rect mOldTargetRect = new Rect();
        private final Rect mNewTargetRect = new Rect();
        private Choreographer mChoreographer;

        // Cached size values from the last render for the case that the view hierarchy is gone
        // during a configuration change.
        private int mLastContentWidth;
        private int mLastContentHeight;
        private int mLastCaptionHeight;
        private int mLastXOffset;
        private int mLastYOffset;

        ResizeFrameThread(ThreadedRenderer renderer, Rect initialBounds) {
            mRenderer = renderer;

            // Create the render nodes for our frame and backdrop which can be resized independently
            // from the content.
            mFrameNode = RenderNode.create("FrameNode", null);
            mBackdropNode = RenderNode.create("BackdropNode", null);

            mRenderer.addRenderNode(mFrameNode, false);
            mRenderer.addRenderNode(mBackdropNode, true);

            // Set the initial bounds and draw once so that we do not get a broken frame.
            mTargetRect.set(initialBounds);
            changeWindowSize(initialBounds);

            // Kick off our draw thread.
            start();
        }

        /**
         * Call this function asynchronously when the window size has been changed. The change will
         * be picked up once per frame and the frame will be re-rendered accordingly.
         * @param newTargetBounds The new target bounds.
         */
        public void setTargetRect(Rect newTargetBounds) {
            synchronized (this) {
                mTargetRect.set(newTargetBounds);
                // Notify of a bounds change.
                pingRenderLocked();
            }
        }

        /**
         * The window got replaced due to a configuration change.
         */
        public void onConfigurationChange() {
            if (mRenderer != null) {
                // Enforce a window redraw.
                mOldTargetRect.set(0, 0, 0, 0);
                pingRenderLocked();
            }
        }

        /**
         * All resources of the renderer will be released. This function can be called from the
         * the UI thread as well as the renderer thread.
         */
        public void releaseRenderer() {
            synchronized (this) {
                if (mRenderer != null) {
                    // Invalidate the current content bounds.
                    mRenderer.setContentDrawBounds(0, 0, 0, 0);

                    // Remove the render nodes again (see comment above - better to do that only once).
                    mRenderer.removeRenderNode(mFrameNode);
                    mRenderer.removeRenderNode(mBackdropNode);

                    mRenderer = null;

                    // Exit the renderer loop.
                    pingRenderLocked();
                }
            }
        }

        @Override
        public void run() {
            try {
                Looper.prepare();
                mChoreographer = Choreographer.getInstance();
                Looper.loop();
            } finally {
                releaseRenderer();
            }
            synchronized (this) {
                // Make sure no more messages are being sent.
                mChoreographer = null;
            }
        }

        /**
         * The implementation of the FrameCallback.
         * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
         * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
         */
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mRenderer == null) {
                // Tell the looper to stop. We are done.
                Looper.myLooper().quit();
                return;
            }
            // Prevent someone from changing this while we are copying.
            synchronized (this) {
                mNewTargetRect.set(mTargetRect);
            }
            if (!mNewTargetRect.equals(mOldTargetRect)) {
                mOldTargetRect.set(mNewTargetRect);
                changeWindowSize(mNewTargetRect);
            }
        }

        /**
         * Resizing the frame to fit the new window size.
         * @param newBounds The window bounds which needs to be drawn.
         */
        private void changeWindowSize(Rect newBounds) {
            long startTime = System.currentTimeMillis();

            // While a configuration change is taking place the view hierarchy might become
            // inaccessible. For that case we remember the previous metrics to avoid flashes.
            View caption = getChildAt(0);
            View content = getChildAt(1);
            if (content != null && caption != null) {
                mLastContentWidth = content.getWidth();
                mLastContentHeight = content.getHeight();
                mLastCaptionHeight = caption.getHeight();

                // Get the draw position within our surface.
                int[] surfaceOrigin = new int[2];
                surfaceOrigin[0] = 0;
                surfaceOrigin[1] = 0;

                // Get the shadow offsets.
                getLocationInSurface(surfaceOrigin);
                mLastXOffset = surfaceOrigin[0];
                mLastYOffset = surfaceOrigin[1];
            }

            // Since the surface is spanning the entire screen, we have to add the start offset of
            // the bounds to get to the surface location.
            final int left = mLastXOffset + newBounds.left;
            final int top = mLastYOffset + newBounds.top;
            final int width = newBounds.width();
            final int height = newBounds.height();

            // Produce the draw calls.
            // TODO(skuhne): Create a separate caption view which draws this. If the shadow should
            // be resized while the window resizes, this hierarchy needs to have the elevation.
            // That said - it is probably no good idea to draw the shadow every time since it costs
            // a considerable time which we should rather spend for resizing the content and it does
            // barely show while the entire screen is moving.
            mFrameNode.setLeftTopRightBottom(left, top, left + width, top + mLastCaptionHeight);
            DisplayListCanvas canvas = mFrameNode.start(width, height);
            canvas.drawColor(Color.BLACK);
            mFrameNode.end(canvas);

            mBackdropNode.setLeftTopRightBottom(left, top + mLastCaptionHeight, left + width,
                    top + height);

            // The backdrop: clear everything with the background. Clipping is done elsewhere.
            canvas = mBackdropNode.start(width, height - mLastCaptionHeight);
            // TODO(skuhne): mOwner.getDecorView().mBackgroundFallback.draw(..) - or similar.
            // Note: This might not work (calculator for example uses a transparent background).
            canvas.drawColor(0xff808080);
            mBackdropNode.end(canvas);

            // The current content buffer is drawn here.
            mRenderer.setContentDrawBounds(
                    mLastXOffset,
                    mLastYOffset + mLastCaptionHeight,
                    mLastXOffset + mLastContentWidth,
                    mLastYOffset + mLastCaptionHeight + mLastContentHeight);

            // We need to render both rendered nodes explicitly.
            mRenderer.drawRenderNode(mFrameNode);
            mRenderer.drawRenderNode(mBackdropNode);
        }

        /**
         * Sends a message to the renderer to wake up and perform the next action which can be
         * either the next rendering or the self destruction if mRenderer is null.
         * Note: This call must be synchronized.
         */
        private void pingRenderLocked() {
            if (mChoreographer != null) {
                mChoreographer.postFrameCallback(this);
            }
        }
    }
}
Loading