| /* |
| * Copyright (C) 2020 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 com.android.cts.graphics.framerateoverride; |
| |
| import static org.junit.Assert.assertTrue; |
| |
| import android.app.Activity; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.hardware.display.DisplayManager; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.support.test.uiautomator.UiDevice; |
| import android.util.Log; |
| import android.view.Choreographer; |
| import android.view.Surface; |
| import android.view.SurfaceHolder; |
| import android.view.SurfaceView; |
| import android.view.ViewGroup; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| |
| /** |
| * An Activity to help with frame rate testing. |
| */ |
| public class FrameRateOverrideTestActivity extends Activity { |
| private static final String TAG = "FrameRateOverrideTestActivity"; |
| private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS = 2 * 1_000_000_000L; |
| private static final long STABLE_FRAME_RATE_WAIT_NANOSECONDS = 1 * 1_000_000_000L; |
| private static final long POST_BUFFER_INTERVAL_NANOSECONDS = 500_000_000L; |
| private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5; |
| private static final long PRECONDITION_WAIT_TIMEOUT_NANOSECONDS = 20 * 1_000_000_000L; |
| private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS = 3 * 1_000_000_000L; |
| private static final float FRAME_RATE_TOLERANCE = 0.01f; |
| private static final float FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE = 5; |
| private static final long FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS = 1 * 1_000_000_000L; |
| private static final long FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS = 10 * 1_000_000_000L; |
| |
| private DisplayManager mDisplayManager; |
| private SurfaceView mSurfaceView; |
| private Handler mHandler = new Handler(Looper.getMainLooper()); |
| private Object mLock = new Object(); |
| private Surface mSurface = null; |
| private float mReportedDisplayRefreshRate; |
| private float mReportedDisplayModeRefreshRate; |
| private ArrayList<Float> mRefreshRateChangedEvents = new ArrayList<Float>(); |
| |
| private long mLastBufferPostTime; |
| |
| SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { |
| @Override |
| public void surfaceCreated(SurfaceHolder holder) { |
| synchronized (mLock) { |
| mSurface = holder.getSurface(); |
| mLock.notify(); |
| } |
| } |
| |
| @Override |
| public void surfaceDestroyed(SurfaceHolder holder) { |
| synchronized (mLock) { |
| mSurface = null; |
| mLock.notify(); |
| } |
| } |
| |
| @Override |
| public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| } |
| }; |
| |
| DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { |
| @Override |
| public void onDisplayAdded(int displayId) { |
| } |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| synchronized (mLock) { |
| float refreshRate = getDisplay().getRefreshRate(); |
| float displayModeRefreshRate = getDisplay().getMode().getRefreshRate(); |
| if (refreshRate != mReportedDisplayRefreshRate |
| || displayModeRefreshRate != mReportedDisplayModeRefreshRate) { |
| Log.i(TAG, String.format("Frame rate changed: (%.2f, %.2f) --> (%.2f, %.2f)", |
| mReportedDisplayModeRefreshRate, |
| mReportedDisplayRefreshRate, |
| displayModeRefreshRate, |
| refreshRate)); |
| mReportedDisplayRefreshRate = refreshRate; |
| mReportedDisplayModeRefreshRate = displayModeRefreshRate; |
| mRefreshRateChangedEvents.add(refreshRate); |
| mLock.notify(); |
| } |
| } |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { |
| } |
| }; |
| |
| private static class PreconditionViolatedException extends RuntimeException { } |
| |
| private static class FrameRateTimeoutException extends RuntimeException { |
| FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate) { |
| this.appRequestedFrameRate = appRequestedFrameRate; |
| this.deviceRefreshRate = deviceRefreshRate; |
| } |
| |
| public float appRequestedFrameRate; |
| public float deviceRefreshRate; |
| } |
| |
| public void postBufferToSurface(int color) { |
| mLastBufferPostTime = System.nanoTime(); |
| Canvas canvas = mSurface.lockCanvas(null); |
| canvas.drawColor(color); |
| mSurface.unlockCanvasAndPost(canvas); |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| synchronized (mLock) { |
| mDisplayManager = getSystemService(DisplayManager.class); |
| mReportedDisplayRefreshRate = getDisplay().getRefreshRate(); |
| mReportedDisplayModeRefreshRate = getDisplay().getMode().getRefreshRate(); |
| mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); |
| mSurfaceView = new SurfaceView(this); |
| mSurfaceView.setWillNotDraw(false); |
| setContentView(mSurfaceView, |
| new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT)); |
| mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| mDisplayManager.unregisterDisplayListener(mDisplayListener); |
| synchronized (mLock) { |
| mLock.notify(); |
| } |
| } |
| |
| private static boolean frameRatesEqual(float frameRate1, float frameRate2) { |
| return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE; |
| } |
| |
| private static boolean frameRatesMatchesOverride(float frameRate1, float frameRate2) { |
| return Math.abs(frameRate1 - frameRate2) <= FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE; |
| } |
| |
| // Waits until our SurfaceHolder has a surface and the activity is resumed. |
| private void waitForPreconditions() throws InterruptedException { |
| assertTrue( |
| "Activity was unexpectedly destroyed", !isDestroyed()); |
| if (mSurface == null || !isResumed()) { |
| Log.i(TAG, String.format( |
| "Waiting for preconditions. Have surface? %b. Activity resumed? %b.", |
| mSurface != null, isResumed())); |
| } |
| long nowNanos = System.nanoTime(); |
| long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_NANOSECONDS; |
| while (mSurface == null || !isResumed()) { |
| long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; |
| assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b." |
| + " Activity resumed? %b.", |
| mSurface != null, isResumed()), |
| timeRemainingMillis > 0); |
| mLock.wait(timeRemainingMillis); |
| assertTrue("Activity was unexpectedly destroyed", !isDestroyed()); |
| nowNanos = System.nanoTime(); |
| } |
| } |
| |
| // Returns true if we encounter a precondition violation, false otherwise. |
| private boolean waitForPreconditionViolation() throws InterruptedException { |
| assertTrue("Activity was unexpectedly destroyed", !isDestroyed()); |
| long nowNanos = System.nanoTime(); |
| long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS; |
| while (mSurface != null && isResumed()) { |
| long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; |
| if (timeRemainingMillis <= 0) { |
| break; |
| } |
| mLock.wait(timeRemainingMillis); |
| assertTrue("Activity was unexpectedly destroyed", !isDestroyed()); |
| nowNanos = System.nanoTime(); |
| } |
| return mSurface == null || !isResumed(); |
| } |
| |
| private void verifyPreconditions() { |
| if (mSurface == null || !isResumed()) { |
| throw new PreconditionViolatedException(); |
| } |
| } |
| |
| // Returns true if we reached waitUntilNanos, false if some other event occurred. |
| private boolean waitForEvents(long waitUntilNanos) |
| throws InterruptedException { |
| mRefreshRateChangedEvents.clear(); |
| long nowNanos = System.nanoTime(); |
| while (nowNanos < waitUntilNanos) { |
| long surfacePostTime = mLastBufferPostTime + POST_BUFFER_INTERVAL_NANOSECONDS; |
| long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos; |
| long timeoutMs = timeoutNs / 1_000_000L; |
| int remainderNs = (int) (timeoutNs % 1_000_000L); |
| // Don't call wait(0, 0) - it blocks indefinitely. |
| if (timeoutMs > 0 || remainderNs > 0) { |
| mLock.wait(timeoutMs, remainderNs); |
| } |
| nowNanos = System.nanoTime(); |
| verifyPreconditions(); |
| if (!mRefreshRateChangedEvents.isEmpty()) { |
| return false; |
| } |
| if (nowNanos >= surfacePostTime) { |
| postBufferToSurface(Color.RED); |
| } |
| } |
| return true; |
| } |
| |
| private void waitForRefreshRateChange(float expectedRefreshRate) throws InterruptedException { |
| Log.i(TAG, "Waiting for the refresh rate to change"); |
| long nowNanos = System.nanoTime(); |
| long gracePeriodEndTimeNanos = |
| nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS; |
| while (true) { |
| // Wait until we switch to the expected refresh rate |
| while (!frameRatesEqual(mReportedDisplayRefreshRate, expectedRefreshRate) |
| && !waitForEvents(gracePeriodEndTimeNanos)) { |
| // Empty |
| } |
| nowNanos = System.nanoTime(); |
| if (nowNanos >= gracePeriodEndTimeNanos) { |
| throw new FrameRateTimeoutException(expectedRefreshRate, |
| mReportedDisplayRefreshRate); |
| } |
| |
| // We've switched to a compatible frame rate. Now wait for a while to see if we stay at |
| // that frame rate. |
| long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_NANOSECONDS; |
| while (endTimeNanos > nowNanos) { |
| if (waitForEvents(endTimeNanos)) { |
| Log.i(TAG, String.format("Stable frame rate %.2f verified", |
| mReportedDisplayRefreshRate)); |
| return; |
| } |
| nowNanos = System.nanoTime(); |
| if (!mRefreshRateChangedEvents.isEmpty()) { |
| break; |
| } |
| } |
| } |
| } |
| |
| interface FrameRateObserver { |
| void observe(float initialRefreshRate, float expectedFrameRate, String condition) |
| throws InterruptedException; |
| } |
| |
| class BackpressureFrameRateObserver implements FrameRateObserver { |
| @Override |
| public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { |
| long startTime = System.nanoTime(); |
| int totalBuffers = 0; |
| float fps = 0; |
| while (System.nanoTime() - startTime <= FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) { |
| postBufferToSurface(Color.BLACK + totalBuffers); |
| totalBuffers++; |
| if (System.nanoTime() - startTime >= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) { |
| float testDuration = (System.nanoTime() - startTime) / 1e9f; |
| fps = totalBuffers / testDuration; |
| if (frameRatesMatchesOverride(fps, expectedFrameRate)) { |
| Log.i(TAG, |
| String.format("%s: backpressure observed refresh rate %.2f", |
| condition, |
| fps)); |
| return; |
| } |
| } |
| } |
| |
| assertTrue(String.format( |
| "%s: backpressure observed refresh rate doesn't match the current refresh " |
| + "rate. " |
| + "expected: %.2f observed: %.2f", condition, expectedFrameRate, fps), |
| frameRatesMatchesOverride(fps, expectedFrameRate)); |
| } |
| } |
| |
| class ChoreographerFrameRateObserver implements FrameRateObserver { |
| class ChoreographerThread extends Thread implements Choreographer.FrameCallback { |
| Choreographer mChoreographer; |
| long mStartTime; |
| public Handler mHandler; |
| Looper mLooper; |
| int mTotalCallbacks = 0; |
| long mEndTime; |
| float mExpectedRefreshRate; |
| String mCondition; |
| |
| ChoreographerThread(float expectedRefreshRate, String condition) |
| throws InterruptedException { |
| mExpectedRefreshRate = expectedRefreshRate; |
| mCondition = condition; |
| } |
| |
| @Override |
| public void run() { |
| Looper.prepare(); |
| mChoreographer = Choreographer.getInstance(); |
| mHandler = new Handler(); |
| mLooper = Looper.myLooper(); |
| mStartTime = System.nanoTime(); |
| mChoreographer.postFrameCallback(this); |
| Looper.loop(); |
| } |
| |
| @Override |
| public void doFrame(long frameTimeNanos) { |
| mTotalCallbacks++; |
| mEndTime = System.nanoTime(); |
| if (mEndTime - mStartTime <= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) { |
| mChoreographer.postFrameCallback(this); |
| return; |
| } else if (frameRatesMatchesOverride(mExpectedRefreshRate, getFps()) |
| || mEndTime - mStartTime > FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) { |
| mLooper.quitSafely(); |
| return; |
| } |
| mChoreographer.postFrameCallback(this); |
| } |
| |
| public void verifyFrameRate() throws InterruptedException { |
| float fps = getFps(); |
| Log.i(TAG, |
| String.format("%s: choreographer observed refresh rate %.2f", |
| mCondition, |
| fps)); |
| assertTrue(String.format( |
| "%s: choreographer observed refresh rate doesn't match the current " |
| + "refresh rate. expected: %.2f observed: %.2f", |
| mCondition, mExpectedRefreshRate, fps), |
| frameRatesMatchesOverride(mExpectedRefreshRate, fps)); |
| } |
| |
| private float getFps() { |
| return mTotalCallbacks / ((mEndTime - mStartTime) / 1e9f); |
| } |
| } |
| |
| @Override |
| public void observe(float initialRefreshRate, float expectedFrameRate, String condition) |
| throws InterruptedException { |
| ChoreographerThread thread = new ChoreographerThread(expectedFrameRate, condition); |
| thread.start(); |
| thread.join(); |
| thread.verifyFrameRate(); |
| } |
| } |
| |
| class DisplayGetRefreshRateFrameRateObserver implements FrameRateObserver { |
| @Override |
| public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { |
| Log.i(TAG, |
| String.format("%s: Display.getRefreshRate() returned refresh rate %.2f", |
| condition, mReportedDisplayRefreshRate)); |
| assertTrue(String.format("%s: Display.getRefreshRate() doesn't match the " |
| + "current refresh. expected: %.2f observed: %.2f", condition, |
| expectedFrameRate, mReportedDisplayRefreshRate), |
| frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedFrameRate)); |
| } |
| } |
| |
| class DisplayModeGetRefreshRateFrameRateObserver implements FrameRateObserver { |
| private final boolean mDisplayModeReturnsPhysicalRefreshRateEnabled; |
| |
| DisplayModeGetRefreshRateFrameRateObserver( |
| boolean displayModeReturnsPhysicalRefreshRateEnabled) { |
| mDisplayModeReturnsPhysicalRefreshRateEnabled = |
| displayModeReturnsPhysicalRefreshRateEnabled; |
| } |
| |
| @Override |
| public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { |
| float expectedDisplayModeRefreshRate = |
| mDisplayModeReturnsPhysicalRefreshRateEnabled ? initialRefreshRate |
| : expectedFrameRate; |
| Log.i(TAG, |
| String.format( |
| "%s: Display.getMode().getRefreshRate() returned refresh rate %.2f", |
| condition, mReportedDisplayModeRefreshRate)); |
| assertTrue(String.format("%s: Display.getMode().getRefreshRate() doesn't match the " |
| + "current refresh. expected: %.2f observed: %.2f", condition, |
| expectedDisplayModeRefreshRate, mReportedDisplayModeRefreshRate), |
| frameRatesMatchesOverride(mReportedDisplayModeRefreshRate, |
| expectedDisplayModeRefreshRate)); |
| } |
| } |
| |
| interface FrameRateOverrideBehavior{ |
| void testFrameRateOverrideBehavior(FrameRateObserver frameRateObserver, |
| float initialRefreshRate) throws InterruptedException, IOException; |
| } |
| |
| class SurfaceFrameRateOverrideBehavior implements FrameRateOverrideBehavior { |
| |
| @Override |
| public void testFrameRateOverrideBehavior(FrameRateObserver frameRateObserver, |
| float initialRefreshRate) throws InterruptedException { |
| Log.i(TAG, "Starting testFrameRateOverride"); |
| float halfFrameRate = initialRefreshRate / 2; |
| |
| waitForRefreshRateChange(initialRefreshRate); |
| frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Initial"); |
| |
| Log.i(TAG, String.format("Setting Frame Rate to %.2f with default compatibility", |
| halfFrameRate)); |
| mSurface.setFrameRate(halfFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, |
| Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); |
| waitForRefreshRateChange(halfFrameRate); |
| frameRateObserver.observe(initialRefreshRate, halfFrameRate, "setFrameRate(default)"); |
| |
| Log.i(TAG, String.format("Setting Frame Rate to %.2f with fixed source compatibility", |
| halfFrameRate)); |
| mSurface.setFrameRate(halfFrameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, |
| Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); |
| waitForRefreshRateChange(halfFrameRate); |
| frameRateObserver.observe(initialRefreshRate, halfFrameRate, |
| "setFrameRate(fixed source)"); |
| |
| Log.i(TAG, "Resetting Frame Rate setting"); |
| mSurface.setFrameRate(0, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, |
| Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); |
| waitForRefreshRateChange(initialRefreshRate); |
| frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Reset"); |
| } |
| } |
| |
| class GameModeFrameRateOverrideBehavior implements FrameRateOverrideBehavior { |
| private UiDevice mUiDevice; |
| GameModeFrameRateOverrideBehavior(UiDevice uiDevice) { |
| mUiDevice = uiDevice; |
| } |
| @Override |
| public void testFrameRateOverrideBehavior(FrameRateObserver frameRateObserver, |
| float initialRefreshRate) throws InterruptedException, IOException { |
| Log.i(TAG, "Starting testGameModeFrameRateOverride"); |
| |
| int initialRefreshRateInt = (int) initialRefreshRate; |
| for (int divisor = 1; initialRefreshRateInt / divisor >= 30; ++divisor) { |
| int overrideFrameRate = initialRefreshRateInt / divisor; |
| Log.i(TAG, String.format("Setting Frame Rate to %d using Game Mode", |
| overrideFrameRate)); |
| |
| mUiDevice.executeShellCommand(String.format("cmd game set --mode 2 --fps %d %s", |
| overrideFrameRate, getPackageName())); |
| waitForRefreshRateChange(overrideFrameRate); |
| frameRateObserver.observe(initialRefreshRate, overrideFrameRate, |
| String.format("Game Mode Override(%d)", overrideFrameRate)); |
| } |
| |
| Log.i(TAG, "Resetting Frame Rate setting"); |
| mUiDevice.executeShellCommand(String.format("cmd game reset %s", getPackageName())); |
| waitForRefreshRateChange(initialRefreshRate); |
| frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Reset"); |
| } |
| } |
| |
| // The activity being intermittently paused/resumed has been observed to |
| // cause test failures in practice, so we run the test with retry logic. |
| public void testFrameRateOverride(FrameRateOverrideBehavior frameRateOverrideBehavior, |
| FrameRateObserver frameRateObserver, float initialRefreshRate) |
| throws InterruptedException, IOException { |
| synchronized (mLock) { |
| Log.i(TAG, "testFrameRateOverride started"); |
| int attempts = 0; |
| boolean testPassed = false; |
| try { |
| while (!testPassed) { |
| waitForPreconditions(); |
| try { |
| frameRateOverrideBehavior.testFrameRateOverrideBehavior(frameRateObserver, |
| initialRefreshRate); |
| testPassed = true; |
| } catch (PreconditionViolatedException exc) { |
| // The logic below will retry if we're below max attempts. |
| } catch (FrameRateTimeoutException exc) { |
| // Sometimes we get a test timeout failure before we get the |
| // notification that the activity was paused, and it was the pause that |
| // caused the timeout failure. Wait for a bit to see if we get notified |
| // of a precondition violation, and if so, retry the test. Otherwise |
| // fail. |
| assertTrue( |
| String.format( |
| "Timed out waiting for a stable and compatible frame" |
| + " rate. requested=%.2f received=%.2f.", |
| exc.appRequestedFrameRate, exc.deviceRefreshRate), |
| waitForPreconditionViolation()); |
| } |
| |
| if (!testPassed) { |
| Log.i(TAG, |
| String.format("Preconditions violated while running the test." |
| + " Have surface? %b. Activity resumed? %b.", |
| mSurface != null, |
| isResumed())); |
| attempts++; |
| assertTrue(String.format( |
| "Exceeded %d precondition wait attempts. Giving up.", |
| PRECONDITION_WAIT_MAX_ATTEMPTS), |
| attempts < PRECONDITION_WAIT_MAX_ATTEMPTS); |
| } |
| } |
| } finally { |
| String passFailMessage = String.format( |
| "%s", testPassed ? "Passed" : "Failed"); |
| if (testPassed) { |
| Log.i(TAG, passFailMessage); |
| } else { |
| Log.e(TAG, passFailMessage); |
| } |
| } |
| |
| } |
| } |
| } |