1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.graphics.framerateoverride; 18 19 import static org.junit.Assert.assertTrue; 20 21 import android.app.Activity; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.hardware.display.DisplayManager; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.support.test.uiautomator.UiDevice; 29 import android.util.Log; 30 import android.view.Choreographer; 31 import android.view.Surface; 32 import android.view.SurfaceHolder; 33 import android.view.SurfaceView; 34 import android.view.ViewGroup; 35 36 import java.io.IOException; 37 import java.util.ArrayList; 38 39 /** 40 * An Activity to help with frame rate testing. 41 */ 42 public class FrameRateOverrideTestActivity extends Activity { 43 private static final String TAG = "FrameRateOverrideTestActivity"; 44 private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS = 2 * 1_000_000_000L; 45 private static final long STABLE_FRAME_RATE_WAIT_NANOSECONDS = 1 * 1_000_000_000L; 46 private static final long POST_BUFFER_INTERVAL_NANOSECONDS = 500_000_000L; 47 private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5; 48 private static final long PRECONDITION_WAIT_TIMEOUT_NANOSECONDS = 20 * 1_000_000_000L; 49 private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS = 3 * 1_000_000_000L; 50 private static final float FRAME_RATE_TOLERANCE = 0.01f; 51 private static final float FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE = 5; 52 private static final long FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS = 1 * 1_000_000_000L; 53 private static final long FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS = 10 * 1_000_000_000L; 54 55 private DisplayManager mDisplayManager; 56 private SurfaceView mSurfaceView; 57 private Handler mHandler = new Handler(Looper.getMainLooper()); 58 private Object mLock = new Object(); 59 private Surface mSurface = null; 60 private float mReportedDisplayRefreshRate; 61 private float mReportedDisplayModeRefreshRate; 62 private ArrayList<Float> mRefreshRateChangedEvents = new ArrayList<Float>(); 63 64 private long mLastBufferPostTime; 65 66 SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 67 @Override 68 public void surfaceCreated(SurfaceHolder holder) { 69 synchronized (mLock) { 70 mSurface = holder.getSurface(); 71 mLock.notify(); 72 } 73 } 74 75 @Override 76 public void surfaceDestroyed(SurfaceHolder holder) { 77 synchronized (mLock) { 78 mSurface = null; 79 mLock.notify(); 80 } 81 } 82 83 @Override 84 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 85 } 86 }; 87 88 DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { 89 @Override 90 public void onDisplayAdded(int displayId) { 91 } 92 93 @Override 94 public void onDisplayChanged(int displayId) { 95 synchronized (mLock) { 96 float refreshRate = getDisplay().getRefreshRate(); 97 float displayModeRefreshRate = getDisplay().getMode().getRefreshRate(); 98 if (refreshRate != mReportedDisplayRefreshRate 99 || displayModeRefreshRate != mReportedDisplayModeRefreshRate) { 100 Log.i(TAG, String.format("Frame rate changed: (%.2f, %.2f) --> (%.2f, %.2f)", 101 mReportedDisplayModeRefreshRate, 102 mReportedDisplayRefreshRate, 103 displayModeRefreshRate, 104 refreshRate)); 105 mReportedDisplayRefreshRate = refreshRate; 106 mReportedDisplayModeRefreshRate = displayModeRefreshRate; 107 mRefreshRateChangedEvents.add(refreshRate); 108 mLock.notify(); 109 } 110 } 111 } 112 113 @Override 114 public void onDisplayRemoved(int displayId) { 115 } 116 }; 117 118 private static class PreconditionViolatedException extends RuntimeException { } 119 120 private static class FrameRateTimeoutException extends RuntimeException { FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate)121 FrameRateTimeoutException(float appRequestedFrameRate, float deviceRefreshRate) { 122 this.appRequestedFrameRate = appRequestedFrameRate; 123 this.deviceRefreshRate = deviceRefreshRate; 124 } 125 126 public float appRequestedFrameRate; 127 public float deviceRefreshRate; 128 } 129 postBufferToSurface(int color)130 public void postBufferToSurface(int color) { 131 mLastBufferPostTime = System.nanoTime(); 132 Canvas canvas = mSurface.lockCanvas(null); 133 canvas.drawColor(color); 134 mSurface.unlockCanvasAndPost(canvas); 135 } 136 137 @Override onCreate(Bundle savedInstanceState)138 protected void onCreate(Bundle savedInstanceState) { 139 super.onCreate(savedInstanceState); 140 synchronized (mLock) { 141 mDisplayManager = getSystemService(DisplayManager.class); 142 mReportedDisplayRefreshRate = getDisplay().getRefreshRate(); 143 mReportedDisplayModeRefreshRate = getDisplay().getMode().getRefreshRate(); 144 mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); 145 mSurfaceView = new SurfaceView(this); 146 mSurfaceView.setWillNotDraw(false); 147 setContentView(mSurfaceView, 148 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 149 ViewGroup.LayoutParams.MATCH_PARENT)); 150 mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); 151 } 152 } 153 154 @Override onDestroy()155 protected void onDestroy() { 156 super.onDestroy(); 157 mDisplayManager.unregisterDisplayListener(mDisplayListener); 158 synchronized (mLock) { 159 mLock.notify(); 160 } 161 } 162 frameRatesEqual(float frameRate1, float frameRate2)163 private static boolean frameRatesEqual(float frameRate1, float frameRate2) { 164 return Math.abs(frameRate1 - frameRate2) <= FRAME_RATE_TOLERANCE; 165 } 166 frameRatesMatchesOverride(float frameRate1, float frameRate2)167 private static boolean frameRatesMatchesOverride(float frameRate1, float frameRate2) { 168 return Math.abs(frameRate1 - frameRate2) <= FPS_TOLERANCE_FOR_FRAME_RATE_OVERRIDE; 169 } 170 171 // Waits until our SurfaceHolder has a surface and the activity is resumed. waitForPreconditions()172 private void waitForPreconditions() throws InterruptedException { 173 assertTrue( 174 "Activity was unexpectedly destroyed", !isDestroyed()); 175 if (mSurface == null || !isResumed()) { 176 Log.i(TAG, String.format( 177 "Waiting for preconditions. Have surface? %b. Activity resumed? %b.", 178 mSurface != null, isResumed())); 179 } 180 long nowNanos = System.nanoTime(); 181 long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_NANOSECONDS; 182 while (mSurface == null || !isResumed()) { 183 long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; 184 assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b." 185 + " Activity resumed? %b.", 186 mSurface != null, isResumed()), 187 timeRemainingMillis > 0); 188 mLock.wait(timeRemainingMillis); 189 assertTrue( 190 "Activity was unexpectedly destroyed", !isDestroyed()); 191 nowNanos = System.nanoTime(); 192 } 193 } 194 195 // Returns true if we encounter a precondition violation, false otherwise. waitForPreconditionViolation()196 private boolean waitForPreconditionViolation() throws InterruptedException { 197 assertTrue( 198 "Activity was unexpectedly destroyed", !isDestroyed()); 199 long nowNanos = System.nanoTime(); 200 long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_NANOSECONDS; 201 while (mSurface != null && isResumed()) { 202 long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; 203 if (timeRemainingMillis <= 0) { 204 break; 205 } 206 mLock.wait(timeRemainingMillis); 207 assertTrue( 208 "Activity was unexpectedly destroyed", !isDestroyed()); 209 nowNanos = System.nanoTime(); 210 } 211 return mSurface == null || !isResumed(); 212 } 213 verifyPreconditions()214 private void verifyPreconditions() { 215 if (mSurface == null || !isResumed()) { 216 throw new PreconditionViolatedException(); 217 } 218 } 219 220 // Returns true if we reached waitUntilNanos, false if some other event occurred. waitForEvents(long waitUntilNanos)221 private boolean waitForEvents(long waitUntilNanos) 222 throws InterruptedException { 223 mRefreshRateChangedEvents.clear(); 224 long nowNanos = System.nanoTime(); 225 while (nowNanos < waitUntilNanos) { 226 long surfacePostTime = mLastBufferPostTime + POST_BUFFER_INTERVAL_NANOSECONDS; 227 long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos; 228 long timeoutMs = timeoutNs / 1_000_000L; 229 int remainderNs = (int) (timeoutNs % 1_000_000L); 230 // Don't call wait(0, 0) - it blocks indefinitely. 231 if (timeoutMs > 0 || remainderNs > 0) { 232 mLock.wait(timeoutMs, remainderNs); 233 } 234 nowNanos = System.nanoTime(); 235 verifyPreconditions(); 236 if (!mRefreshRateChangedEvents.isEmpty()) { 237 return false; 238 } 239 if (nowNanos >= surfacePostTime) { 240 postBufferToSurface(Color.RED); 241 } 242 } 243 return true; 244 } 245 waitForRefreshRateChange(float expectedRefreshRate)246 private void waitForRefreshRateChange(float expectedRefreshRate) throws InterruptedException { 247 Log.i(TAG, "Waiting for the refresh rate to change"); 248 long nowNanos = System.nanoTime(); 249 long gracePeriodEndTimeNanos = 250 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_NANOSECONDS; 251 while (true) { 252 // Wait until we switch to the expected refresh rate 253 while (!frameRatesEqual(mReportedDisplayRefreshRate, expectedRefreshRate) 254 && !waitForEvents(gracePeriodEndTimeNanos)) { 255 // Empty 256 } 257 nowNanos = System.nanoTime(); 258 if (nowNanos >= gracePeriodEndTimeNanos) { 259 throw new FrameRateTimeoutException(expectedRefreshRate, 260 mReportedDisplayRefreshRate); 261 } 262 263 // We've switched to a compatible frame rate. Now wait for a while to see if we stay at 264 // that frame rate. 265 long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_NANOSECONDS; 266 while (endTimeNanos > nowNanos) { 267 if (waitForEvents(endTimeNanos)) { 268 Log.i(TAG, String.format("Stable frame rate %.2f verified", 269 mReportedDisplayRefreshRate)); 270 return; 271 } 272 nowNanos = System.nanoTime(); 273 if (!mRefreshRateChangedEvents.isEmpty()) { 274 break; 275 } 276 } 277 } 278 } 279 280 interface FrameRateObserver { observe(float initialRefreshRate, float expectedFrameRate, String condition)281 void observe(float initialRefreshRate, float expectedFrameRate, String condition) 282 throws InterruptedException; 283 } 284 285 class BackpressureFrameRateObserver implements FrameRateObserver { 286 @Override observe(float initialRefreshRate, float expectedFrameRate, String condition)287 public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { 288 long startTime = System.nanoTime(); 289 int totalBuffers = 0; 290 float fps = 0; 291 while (System.nanoTime() - startTime <= FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) { 292 postBufferToSurface(Color.BLACK + totalBuffers); 293 totalBuffers++; 294 if (System.nanoTime() - startTime >= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) { 295 float testDuration = (System.nanoTime() - startTime) / 1e9f; 296 fps = totalBuffers / testDuration; 297 if (frameRatesMatchesOverride(fps, expectedFrameRate)) { 298 Log.i(TAG, 299 String.format("%s: backpressure observed refresh rate %.2f", 300 condition, 301 fps)); 302 return; 303 } 304 } 305 } 306 307 assertTrue(String.format( 308 "%s: backpressure observed refresh rate doesn't match the current refresh " 309 + "rate. " 310 + "expected: %.2f observed: %.2f", condition, expectedFrameRate, fps), 311 frameRatesMatchesOverride(fps, expectedFrameRate)); 312 } 313 } 314 315 class ChoreographerFrameRateObserver implements FrameRateObserver { 316 class ChoreographerThread extends Thread implements Choreographer.FrameCallback { 317 Choreographer mChoreographer; 318 long mStartTime; 319 public Handler mHandler; 320 Looper mLooper; 321 int mTotalCallbacks = 0; 322 long mEndTime; 323 float mExpectedRefreshRate; 324 String mCondition; 325 ChoreographerThread(float expectedRefreshRate, String condition)326 ChoreographerThread(float expectedRefreshRate, String condition) 327 throws InterruptedException { 328 mExpectedRefreshRate = expectedRefreshRate; 329 mCondition = condition; 330 } 331 332 @Override run()333 public void run() { 334 Looper.prepare(); 335 mChoreographer = Choreographer.getInstance(); 336 mHandler = new Handler(); 337 mLooper = Looper.myLooper(); 338 mStartTime = System.nanoTime(); 339 mChoreographer.postFrameCallback(this); 340 Looper.loop(); 341 } 342 343 @Override doFrame(long frameTimeNanos)344 public void doFrame(long frameTimeNanos) { 345 mTotalCallbacks++; 346 mEndTime = System.nanoTime(); 347 if (mEndTime - mStartTime <= FRAME_RATE_MIN_WAIT_TIME_NANOSECONDS) { 348 mChoreographer.postFrameCallback(this); 349 return; 350 } else if (frameRatesMatchesOverride(mExpectedRefreshRate, getFps()) 351 || mEndTime - mStartTime > FRAME_RATE_MAX_WAIT_TIME_NANOSECONDS) { 352 mLooper.quitSafely(); 353 return; 354 } 355 mChoreographer.postFrameCallback(this); 356 } 357 verifyFrameRate()358 public void verifyFrameRate() throws InterruptedException { 359 float fps = getFps(); 360 Log.i(TAG, 361 String.format("%s: choreographer observed refresh rate %.2f", 362 mCondition, 363 fps)); 364 assertTrue(String.format( 365 "%s: choreographer observed refresh rate doesn't match the current " 366 + "refresh rate. expected: %.2f observed: %.2f", 367 mCondition, mExpectedRefreshRate, fps), 368 frameRatesMatchesOverride(mExpectedRefreshRate, fps)); 369 } 370 getFps()371 private float getFps() { 372 return mTotalCallbacks / ((mEndTime - mStartTime) / 1e9f); 373 } 374 } 375 376 @Override observe(float initialRefreshRate, float expectedFrameRate, String condition)377 public void observe(float initialRefreshRate, float expectedFrameRate, String condition) 378 throws InterruptedException { 379 ChoreographerThread thread = new ChoreographerThread(expectedFrameRate, condition); 380 thread.start(); 381 thread.join(); 382 thread.verifyFrameRate(); 383 } 384 } 385 386 class DisplayGetRefreshRateFrameRateObserver implements FrameRateObserver { 387 @Override observe(float initialRefreshRate, float expectedFrameRate, String condition)388 public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { 389 Log.i(TAG, 390 String.format("%s: Display.getRefreshRate() returned refresh rate %.2f", 391 condition, mReportedDisplayRefreshRate)); 392 assertTrue(String.format("%s: Display.getRefreshRate() doesn't match the " 393 + "current refresh. expected: %.2f observed: %.2f", condition, 394 expectedFrameRate, mReportedDisplayRefreshRate), 395 frameRatesMatchesOverride(mReportedDisplayRefreshRate, expectedFrameRate)); 396 } 397 } 398 class DisplayModeGetRefreshRateFrameRateObserver implements FrameRateObserver { 399 private final boolean mDisplayModeReturnsPhysicalRefreshRateEnabled; 400 DisplayModeGetRefreshRateFrameRateObserver( boolean displayModeReturnsPhysicalRefreshRateEnabled)401 DisplayModeGetRefreshRateFrameRateObserver( 402 boolean displayModeReturnsPhysicalRefreshRateEnabled) { 403 mDisplayModeReturnsPhysicalRefreshRateEnabled = 404 displayModeReturnsPhysicalRefreshRateEnabled; 405 } 406 407 @Override observe(float initialRefreshRate, float expectedFrameRate, String condition)408 public void observe(float initialRefreshRate, float expectedFrameRate, String condition) { 409 float expectedDisplayModeRefreshRate = 410 mDisplayModeReturnsPhysicalRefreshRateEnabled ? initialRefreshRate 411 : expectedFrameRate; 412 Log.i(TAG, 413 String.format( 414 "%s: Display.getMode().getRefreshRate() returned refresh rate %.2f", 415 condition, mReportedDisplayModeRefreshRate)); 416 assertTrue(String.format("%s: Display.getMode().getRefreshRate() doesn't match the " 417 + "current refresh. expected: %.2f observed: %.2f", condition, 418 expectedDisplayModeRefreshRate, mReportedDisplayModeRefreshRate), 419 frameRatesMatchesOverride(mReportedDisplayModeRefreshRate, 420 expectedDisplayModeRefreshRate)); 421 } 422 } 423 424 interface TestScenario { test(FrameRateObserver frameRateObserver, float initialRefreshRate)425 void test(FrameRateObserver frameRateObserver, 426 float initialRefreshRate) throws InterruptedException, IOException; 427 } 428 429 class GameModeTest implements TestScenario { 430 private UiDevice mUiDevice; GameModeTest(UiDevice uiDevice)431 GameModeTest(UiDevice uiDevice) { 432 mUiDevice = uiDevice; 433 } 434 @Override test(FrameRateObserver frameRateObserver, float initialRefreshRate)435 public void test(FrameRateObserver frameRateObserver, 436 float initialRefreshRate) throws InterruptedException, IOException { 437 Log.i(TAG, "Starting testGameModeFrameRateOverride"); 438 439 int initialRefreshRateInt = (int) initialRefreshRate; 440 for (int divisor = 1; initialRefreshRateInt / divisor >= 30; ++divisor) { 441 int overrideFrameRate = initialRefreshRateInt / divisor; 442 if (initialRefreshRateInt % divisor != 0) { 443 // skip if the overriding frame rate is not a divisor of initial refresh rate 444 Log.i(TAG, String.format("Skipping Frame rate %d as it is not a divisor of" 445 + " refresh rate of %d", overrideFrameRate, initialRefreshRateInt)); 446 continue; 447 } 448 Log.i(TAG, String.format("Setting Frame Rate to %d using Game Mode", 449 overrideFrameRate)); 450 451 mUiDevice.executeShellCommand(String.format("cmd game set --mode 2 --fps %d %s", 452 overrideFrameRate, getPackageName())); 453 waitForRefreshRateChange(overrideFrameRate); 454 frameRateObserver.observe(initialRefreshRate, overrideFrameRate, 455 String.format("Game Mode Override(%d)", overrideFrameRate)); 456 } 457 458 Log.i(TAG, "Resetting Frame Rate setting"); 459 mUiDevice.executeShellCommand(String.format("cmd game reset %s", getPackageName())); 460 waitForRefreshRateChange(initialRefreshRate); 461 frameRateObserver.observe(initialRefreshRate, initialRefreshRate, "Reset"); 462 } 463 } 464 465 // The activity being intermittently paused/resumed has been observed to 466 // cause test failures in practice, so we run the test with retry logic. testFrameRateOverride(TestScenario frameRateOverrideBehavior, FrameRateObserver frameRateObserver, float initialRefreshRate)467 public void testFrameRateOverride(TestScenario frameRateOverrideBehavior, 468 FrameRateObserver frameRateObserver, float initialRefreshRate) 469 throws InterruptedException, IOException { 470 synchronized (mLock) { 471 Log.i(TAG, "testFrameRateOverride started with initial refresh rate " 472 + initialRefreshRate); 473 int attempts = 0; 474 boolean testPassed = false; 475 try { 476 while (!testPassed) { 477 waitForPreconditions(); 478 try { 479 frameRateOverrideBehavior.test(frameRateObserver, 480 initialRefreshRate); 481 testPassed = true; 482 } catch (PreconditionViolatedException exc) { 483 // The logic below will retry if we're below max attempts. 484 } catch (FrameRateTimeoutException exc) { 485 // Sometimes we get a test timeout failure before we get the 486 // notification that the activity was paused, and it was the pause that 487 // caused the timeout failure. Wait for a bit to see if we get notified 488 // of a precondition violation, and if so, retry the test. Otherwise 489 // fail. 490 assertTrue( 491 String.format( 492 "Timed out waiting for a stable and compatible frame" 493 + " rate. requested=%.2f received=%.2f.", 494 exc.appRequestedFrameRate, exc.deviceRefreshRate), 495 waitForPreconditionViolation()); 496 } 497 498 if (!testPassed) { 499 Log.i(TAG, 500 String.format("Preconditions violated while running the test." 501 + " Have surface? %b. Activity resumed? %b.", 502 mSurface != null, 503 isResumed())); 504 attempts++; 505 assertTrue(String.format( 506 "Exceeded %d precondition wait attempts. Giving up.", 507 PRECONDITION_WAIT_MAX_ATTEMPTS), 508 attempts < PRECONDITION_WAIT_MAX_ATTEMPTS); 509 } 510 } 511 } finally { 512 if (testPassed) { 513 Log.i(TAG, "**** PASS ****"); 514 } else { 515 Log.i(TAG, "**** FAIL ****"); 516 } 517 } 518 519 } 520 } 521 } 522