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

Commit f33d1634 authored by Riddle Hsu's avatar Riddle Hsu
Browse files

Optimize luminance calculation for rotation animation

- Apply sampling for the border to reduce the amonut of pixel
  for calculating luminance.
- Use getInt to get color rather than get a byte 4 times.
- Use a simpler formula to approximate luminance.
- Replace median with mode to represent the luminance. It is more
  efficient (no need sort) and the visual result may be better if
  the distribution is U-shaped.
- Extract common code to policy package to reduce duplicated
  code in system server and shell.

The calculation is 10 times faster for the first execution,
and 5 times faster on average.

Bug: 253610885
Test: atest RotationAnimationUtilsTest

Change-Id: Ieca9968e843b68d733e178edd725b628c542e872
parent 58946d06
Loading
Loading
Loading
Loading
+92 −0
Original line number Diff line number Diff line
@@ -41,12 +41,16 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorSpace;
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.hardware.HardwareBuffer;
import android.media.Image;
import android.media.ImageReader;
import android.os.SystemProperties;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.TransitionOldType;
import android.view.WindowManager.TransitionType;
@@ -59,9 +63,11 @@ import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.window.ScreenCapture;

import com.android.internal.R;

import java.nio.ByteBuffer;
import java.util.List;

/** @hide */
@@ -1262,4 +1268,90 @@ public class TransitionAnimation {

        return set;
    }

    /** Returns whether the hardware buffer passed in is marked as protected. */
    public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
        return (hardwareBuffer.getUsage() & HardwareBuffer.USAGE_PROTECTED_CONTENT)
                == HardwareBuffer.USAGE_PROTECTED_CONTENT;
    }

    /** Returns the luminance in 0~1. */
    public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) {
        final ScreenCapture.ScreenshotHardwareBuffer buffer =
                ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1);
        if (buffer != null) {
            return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
        }
        return 0;
    }

    /** Returns the luminance in 0~1. */
    public static float getBorderLuma(HardwareBuffer hwBuffer, ColorSpace colorSpace) {
        if (hwBuffer == null) {
            return 0;
        }
        final int format = hwBuffer.getFormat();
        // Only support RGB format in 4 bytes. And protected buffer is not readable.
        if (format != HardwareBuffer.RGBA_8888 || hasProtectedContent(hwBuffer)) {
            return 0;
        }

        final ImageReader ir = ImageReader.newInstance(hwBuffer.getWidth(), hwBuffer.getHeight(),
                format, 1 /* maxImages */);
        ir.getSurface().attachAndQueueBufferWithColorSpace(hwBuffer, colorSpace);
        final Image image = ir.acquireLatestImage();
        if (image == null || image.getPlaneCount() < 1) {
            return 0;
        }

        final Image.Plane plane = image.getPlanes()[0];
        final ByteBuffer buffer = plane.getBuffer();
        final int width = image.getWidth();
        final int height = image.getHeight();
        final int pixelStride = plane.getPixelStride();
        final int rowStride = plane.getRowStride();
        final int sampling = 10;
        final int[] borderLumas = new int[(width + height) * 2 / sampling];

        // Grab the top and bottom borders.
        int i = 0;
        for (int x = 0, size = width - sampling; x < size; x += sampling) {
            borderLumas[i++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
            borderLumas[i++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
        }

        // Grab the left and right borders.
        for (int y = 0, size = height - sampling; y < size; y += sampling) {
            borderLumas[i++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
            borderLumas[i++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
        }

        ir.close();

        // Get "mode" by histogram.
        final int[] histogram = new int[256];
        int maxCount = 0;
        int mostLuma = 0;
        for (int luma : borderLumas) {
            final int count = ++histogram[luma];
            if (count > maxCount) {
                maxCount = count;
                mostLuma = luma;
            }
        }
        return mostLuma / 255f;
    }

    /** Returns the luminance of the pixel in 0~255. */
    private static int getPixelLuminance(ByteBuffer buffer, int x, int y, int pixelStride,
            int rowStride) {
        final int color = buffer.getInt(y * rowStride + x * pixelStride);
        // The buffer from ImageReader is always in native order (little-endian), so extract the
        // color components in reversed order.
        final int r = color & 0xff;
        final int g = (color >> 8) & 0xff;
        final int b = (color >> 16) & 0xff;
        // Approximation of WCAG 2.0 relative luminance.
        return ((r * 8) + (g * 22) + (b * 2)) >> 5;
    }
}
+2 −94
Original line number Diff line number Diff line
@@ -16,8 +16,6 @@

package com.android.wm.shell.transition;

import static android.hardware.HardwareBuffer.RGBA_8888;
import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
import static android.util.RotationUtils.deltaRotation;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
@@ -37,8 +35,6 @@ import android.graphics.ColorSpace;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.media.Image;
import android.media.ImageReader;
import android.util.Slog;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -50,12 +46,11 @@ import android.window.ScreenCapture;
import android.window.TransitionInfo;

import com.android.internal.R;
import com.android.internal.policy.TransitionAnimation;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * This class handles the rotation animation when the device is rotated.
@@ -173,7 +168,7 @@ class ScreenRotationAnimation {
                t.setBuffer(mScreenshotLayer, hardwareBuffer);
                t.show(mScreenshotLayer);
                if (!isCustomRotate()) {
                    mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
                    mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace);
                }
            }

@@ -404,93 +399,6 @@ class ScreenRotationAnimation {
        mTransactionPool.release(t);
    }

    /**
     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
     * luminance at the borders of the bitmap
     * @return the average luminance of all the pixels at the borders of the bitmap
     */
    private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
        // Cannot read content from buffer with protected usage.
        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
                || hasProtectedContent(hardwareBuffer)) {
            return 0;
        }

        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
        Image image = ir.acquireLatestImage();
        if (image == null || image.getPlanes().length == 0) {
            return 0;
        }

        Image.Plane plane = image.getPlanes()[0];
        ByteBuffer buffer = plane.getBuffer();
        int width = image.getWidth();
        int height = image.getHeight();
        int pixelStride = plane.getPixelStride();
        int rowStride = plane.getRowStride();
        float[] borderLumas = new float[2 * width + 2 * height];

        // Grab the top and bottom borders
        int l = 0;
        for (int x = 0; x < width; x++) {
            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
        }

        // Grab the left and right borders
        for (int y = 0; y < height; y++) {
            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
        }

        // Cleanup
        ir.close();

        // Oh, is this too simple and inefficient for you?
        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
        Arrays.sort(borderLumas);
        return borderLumas[borderLumas.length / 2];
    }

    /**
     * @return whether the hardwareBuffer passed in is marked as protected.
     */
    private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
    }

    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
            int pixelStride, int rowStride) {
        int offset = y * rowStride + x * pixelStride;
        int pixel = 0;
        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
        pixel |= (buffer.get(offset + 2) & 0xff);       // B
        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
        return Color.valueOf(pixel).luminance();
    }

    /**
     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
     */
    private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
        if (surfaceControl ==  null) {
            return 0;
        }

        Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
        ScreenCapture.ScreenshotHardwareBuffer buffer =
                ScreenCapture.captureLayers(surfaceControl, crop, 1);
        if (buffer == null) {
            return 0;
        }

        return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
    }

    private static void applyColor(int startColor, int endColor, float[] rgbFloat,
            float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
        final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
+4 −3
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import android.view.animation.Transformation;
import android.window.ScreenCapture;

import com.android.internal.R;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.display.DisplayControl;
import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -246,7 +247,7 @@ class ScreenRotationAnimation {
            HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                    "ScreenRotationAnimation#getMedianBorderLuma");
            mStartLuma = RotationAnimationUtils.getMedianBorderLuma(hardwareBuffer,
            mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
                    screenshotBuffer.getColorSpace());
            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);

@@ -489,8 +490,8 @@ class ScreenRotationAnimation {
            return false;
        }
        if (!mStarted) {
            mEndLuma = RotationAnimationUtils.getLumaOfSurfaceControl(mDisplayContent.getDisplay(),
                    mDisplayContent.getWindowingLayer());
            mEndLuma = TransitionAnimation.getBorderLuma(mDisplayContent.getWindowingLayer(),
                    finalWidth, finalHeight);
            startAnimation(t, maxAnimationDuration, animationScale, finalWidth, finalHeight,
                    exitAnim, enterAnim);
        }
+2 −2
Original line number Diff line number Diff line
@@ -83,11 +83,11 @@ import android.window.TransitionInfo;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.wm.utils.RotationAnimationUtils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -2190,7 +2190,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe
            changeInfo.mSnapshot = snapshotSurface;
            if (isDisplayRotation) {
                // This isn't cheap, so only do it for display rotations.
                changeInfo.mSnapshotLuma = RotationAnimationUtils.getMedianBorderLuma(
                changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
                        screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace());
            }
            SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
+0 −96
Original line number Diff line number Diff line
@@ -16,24 +16,11 @@

package com.android.server.wm.utils;

import static android.hardware.HardwareBuffer.RGBA_8888;
import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;

import android.graphics.Color;
import android.graphics.ColorSpace;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.media.Image;
import android.media.ImageReader;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.ScreenCapture;

import java.nio.ByteBuffer;
import java.util.Arrays;


/** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/
@@ -46,89 +33,6 @@ public class RotationAnimationUtils {
        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
    }

    /**
     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
     * luminance at the borders of the bitmap
     * @return the average luminance of all the pixels at the borders of the bitmap
     */
    public static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
        // Cannot read content from buffer with protected usage.
        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
                || hasProtectedContent(hardwareBuffer)) {
            return 0;
        }

        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
        Image image = ir.acquireLatestImage();
        if (image == null || image.getPlanes().length == 0) {
            return 0;
        }

        Image.Plane plane = image.getPlanes()[0];
        ByteBuffer buffer = plane.getBuffer();
        int width = image.getWidth();
        int height = image.getHeight();
        int pixelStride = plane.getPixelStride();
        int rowStride = plane.getRowStride();
        float[] borderLumas = new float[2 * width + 2 * height];

        // Grab the top and bottom borders
        int l = 0;
        for (int x = 0; x < width; x++) {
            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
        }

        // Grab the left and right borders
        for (int y = 0; y < height; y++) {
            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
        }

        // Cleanup
        ir.close();

        // Oh, is this too simple and inefficient for you?
        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
        Arrays.sort(borderLumas);
        return borderLumas[borderLumas.length / 2];
    }

    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
            int pixelStride, int rowStride) {
        int offset = y * rowStride + x * pixelStride;
        int pixel = 0;
        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
        pixel |= (buffer.get(offset + 2) & 0xff);       // B
        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
        return Color.valueOf(pixel).luminance();
    }

    /**
     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
     */
    public static float getLumaOfSurfaceControl(Display display, SurfaceControl surfaceControl) {
        if (surfaceControl ==  null) {
            return 0;
        }

        Point size = new Point();
        display.getSize(size);
        Rect crop = new Rect(0, 0, size.x, size.y);
        ScreenCapture.ScreenshotHardwareBuffer buffer =
                ScreenCapture.captureLayers(surfaceControl, crop, 1);
        if (buffer == null) {
            return 0;
        }

        return RotationAnimationUtils.getMedianBorderLuma(buffer.getHardwareBuffer(),
                buffer.getColorSpace());
    }

    public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) {
        switch (rotation) {
            case Surface.ROTATION_0:
Loading