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 android.graphics.cts; 18 19 import static android.system.OsConstants.EINVAL; 20 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertNotSame; 24 import static org.junit.Assert.assertTrue; 25 26 import android.app.Activity; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Rect; 30 import android.hardware.display.DisplayManager; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.util.Log; 35 import android.view.Display; 36 import android.view.Surface; 37 import android.view.SurfaceControl; 38 import android.view.SurfaceHolder; 39 import android.view.SurfaceView; 40 import android.view.ViewGroup; 41 42 import com.android.compatibility.common.util.DisplayUtil; 43 44 import com.google.common.primitives.Floats; 45 46 import java.io.PrintWriter; 47 import java.io.StringWriter; 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.List; 51 52 /** 53 * An Activity to help with frame rate testing. 54 */ 55 public class FrameRateCtsActivity extends Activity { 56 static { 57 System.loadLibrary("ctsgraphics_jni"); 58 } 59 60 private static final String TAG = "FrameRateCtsActivity"; 61 private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS = 4; 62 private static final long STABLE_FRAME_RATE_WAIT_SECONDS = 1; 63 private static final long POST_BUFFER_INTERVAL_MILLIS = 500; 64 private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5; 65 private static final long PRECONDITION_WAIT_TIMEOUT_SECONDS = 20; 66 private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS = 3; 67 private static final float FRAME_RATE_TOLERANCE_STRICT = 0.01f; 68 69 // Tolerance which doesn't differentiate between the fractional refresh rate pairs, e.g. 70 // 59.94 and 60 will be considered the same refresh rate. 71 // Use this tolerance to verify the refresh rate after calling setFrameRate with 72 // {@Surface.FRAME_RATE_COMPATIBILITY_DEFAULT}. 73 private static final float FRAME_RATE_TOLERANCE_RELAXED = 0.1f; 74 75 private DisplayManager mDisplayManager; 76 private SurfaceView mSurfaceView; 77 private Handler mHandler = new Handler(Looper.getMainLooper()); 78 private final Object mLock = new Object(); 79 private Surface mSurface = null; 80 private float mDeviceFrameRate; 81 private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents(); 82 83 private enum ActivityState { RUNNING, PAUSED, DESTROYED } 84 85 private ActivityState mActivityState; 86 87 SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 88 @Override 89 public void surfaceCreated(SurfaceHolder holder) { 90 synchronized (mLock) { 91 mSurface = holder.getSurface(); 92 mLock.notify(); 93 } 94 } 95 96 @Override 97 public void surfaceDestroyed(SurfaceHolder holder) { 98 synchronized (mLock) { 99 mSurface = null; 100 mLock.notify(); 101 } 102 } 103 104 @Override 105 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} 106 }; 107 108 DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { 109 @Override 110 public void onDisplayAdded(int displayId) {} 111 112 @Override 113 public void onDisplayChanged(int displayId) { 114 if (displayId != Display.DEFAULT_DISPLAY) { 115 return; 116 } 117 synchronized (mLock) { 118 Display.Mode mode = mDisplayManager.getDisplay(displayId).getMode(); 119 mModeChangedEvents.add(mode); 120 float frameRate = mode.getRefreshRate(); 121 if (frameRate != mDeviceFrameRate) { 122 Log.i(TAG, 123 String.format("Frame rate changed: %.2f --> %.2f", mDeviceFrameRate, 124 frameRate)); 125 mDeviceFrameRate = frameRate; 126 mLock.notify(); 127 } 128 } 129 } 130 131 @Override 132 public void onDisplayRemoved(int displayId) {} 133 }; 134 135 // Wrapper around ArrayList for which the only allowed mutable operation is add(). 136 // We use this to store all mode change events during a test. When we need to iterate over 137 // all mode changes during a certain operation, we use the number of events in the beginning 138 // and in the end. It's important to never clear or modify the elements in this list hence the 139 // wrapper. 140 private static class ModeChangedEvents { 141 private List<Display.Mode> mEvents = new ArrayList<>(); 142 add(Display.Mode mode)143 public void add(Display.Mode mode) { 144 mEvents.add(mode); 145 } 146 get(int i)147 public Display.Mode get(int i) { 148 return mEvents.get(i); 149 } 150 size()151 public int size() { 152 return mEvents.size(); 153 } 154 } 155 156 private static class PreconditionViolatedException extends RuntimeException { PreconditionViolatedException()157 PreconditionViolatedException() {} 158 } 159 160 private static class FrameRateTimeoutException extends RuntimeException { FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate)161 FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate) { 162 this.expectedFrameRate = expectedFrameRate; 163 this.deviceFrameRate = deviceFrameRate; 164 } 165 166 public float expectedFrameRate; 167 public float deviceFrameRate; 168 } 169 170 public enum Api { 171 SURFACE("Surface"), 172 ANATIVE_WINDOW("ANativeWindow"), 173 SURFACE_CONTROL("SurfaceControl"), 174 NATIVE_SURFACE_CONTROL("ASurfaceControl"); 175 176 private final String mName; Api(String name)177 Api(String name) { 178 mName = name; 179 } 180 toString()181 public String toString() { 182 return mName; 183 } 184 } 185 186 private static class TestSurface { 187 private Api mApi; 188 private String mName; 189 private SurfaceControl mSurfaceControl; 190 private Surface mSurface; 191 private long mNativeSurfaceControl; 192 private int mColor; 193 private boolean mLastBufferPostTimeValid; 194 private long mLastBufferPostTime; 195 TestSurface(Api api, SurfaceControl parentSurfaceControl, Surface parentSurface, String name, Rect destFrame, boolean visible, int color)196 TestSurface(Api api, SurfaceControl parentSurfaceControl, Surface parentSurface, 197 String name, Rect destFrame, boolean visible, int color) { 198 mApi = api; 199 mName = name; 200 mColor = color; 201 202 if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) { 203 assertNotNull("No parent surface", parentSurfaceControl); 204 mSurfaceControl = new SurfaceControl.Builder() 205 .setParent(parentSurfaceControl) 206 .setName(mName) 207 .setBufferSize(destFrame.right - destFrame.left, 208 destFrame.bottom - destFrame.top) 209 .build(); 210 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 211 transaction.setGeometry(mSurfaceControl, null, destFrame, Surface.ROTATION_0) 212 .apply(); 213 } 214 mSurface = new Surface(mSurfaceControl); 215 } else if (mApi == Api.NATIVE_SURFACE_CONTROL) { 216 assertNotNull("No parent surface", parentSurface); 217 mNativeSurfaceControl = nativeSurfaceControlCreate(parentSurface, mName, 218 destFrame.left, destFrame.top, destFrame.right, destFrame.bottom); 219 assertTrue("Failed to create a native SurfaceControl", mNativeSurfaceControl != 0); 220 } 221 222 setVisibility(visible); 223 postBuffer(); 224 } 225 setFrameRate(float frameRate, int compatibility, int changeFrameRateStrategy)226 public int setFrameRate(float frameRate, int compatibility, int changeFrameRateStrategy) { 227 Log.i(TAG, 228 String.format("Setting frame rate for %s: fps=%.2f compatibility=%s", mName, 229 frameRate, frameRateCompatibilityToString(compatibility))); 230 231 int rc = 0; 232 if (mApi == Api.SURFACE) { 233 if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) { 234 mSurface.setFrameRate(frameRate, compatibility); 235 } else { 236 mSurface.setFrameRate(frameRate, compatibility, changeFrameRateStrategy); 237 } 238 } else if (mApi == Api.ANATIVE_WINDOW) { 239 rc = nativeWindowSetFrameRate(mSurface, frameRate, compatibility, 240 changeFrameRateStrategy); 241 } else if (mApi == Api.SURFACE_CONTROL) { 242 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 243 if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) { 244 transaction 245 .setFrameRate(mSurfaceControl, frameRate, compatibility); 246 } else { 247 transaction 248 .setFrameRate(mSurfaceControl, frameRate, compatibility, 249 changeFrameRateStrategy); 250 } 251 transaction.apply(); 252 } 253 } else if (mApi == Api.NATIVE_SURFACE_CONTROL) { 254 nativeSurfaceControlSetFrameRate(mNativeSurfaceControl, frameRate, compatibility, 255 changeFrameRateStrategy); 256 } 257 return rc; 258 } 259 clearFrameRate()260 public int clearFrameRate() { 261 Log.i(TAG, 262 String.format("Clearing frame rate for %s", mName)); 263 int rc = 0; 264 if (mApi == Api.SURFACE) { 265 mSurface.clearFrameRate(); 266 } else if (mApi == Api.ANATIVE_WINDOW) { 267 rc = nativeWindowClearFrameRate(mSurface); 268 } else if (mApi == Api.SURFACE_CONTROL) { 269 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 270 transaction.clearFrameRate(mSurfaceControl); 271 transaction.apply(); 272 } 273 } else if (mApi == Api.NATIVE_SURFACE_CONTROL) { 274 nativeSurfaceControlClearFrameRate(mNativeSurfaceControl); 275 } 276 return rc; 277 } 278 setInvalidFrameRate(float frameRate, int compatibility, int changeFrameRateStrategy)279 public void setInvalidFrameRate(float frameRate, int compatibility, 280 int changeFrameRateStrategy) { 281 if (mApi == Api.SURFACE) { 282 boolean caughtIllegalArgException = false; 283 try { 284 setFrameRate(frameRate, compatibility, changeFrameRateStrategy); 285 } catch (IllegalArgumentException exc) { 286 caughtIllegalArgException = true; 287 } 288 assertTrue("Expected an IllegalArgumentException from invalid call to" 289 + " Surface.setFrameRate()", 290 caughtIllegalArgException); 291 } else { 292 int rc = setFrameRate(frameRate, compatibility, changeFrameRateStrategy); 293 if (mApi == Api.ANATIVE_WINDOW) { 294 assertEquals("Expected -EINVAL return value from invalid call to" 295 + " ANativeWindow_setFrameRate()", rc, -EINVAL); 296 } 297 } 298 } 299 setVisibility(boolean visible)300 public void setVisibility(boolean visible) { 301 Log.i(TAG, 302 String.format("Setting visibility for %s: %s", mName, 303 visible ? "visible" : "hidden")); 304 if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) { 305 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 306 transaction.setVisibility(mSurfaceControl, visible).apply(); 307 } 308 } else if (mApi == Api.NATIVE_SURFACE_CONTROL) { 309 nativeSurfaceControlSetVisibility(mNativeSurfaceControl, visible); 310 } 311 } 312 postBuffer()313 public void postBuffer() { 314 mLastBufferPostTimeValid = true; 315 mLastBufferPostTime = System.nanoTime(); 316 if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) { 317 Canvas canvas = mSurface.lockHardwareCanvas(); 318 canvas.drawColor(mColor); 319 mSurface.unlockCanvasAndPost(canvas); 320 } else if (mApi == Api.NATIVE_SURFACE_CONTROL) { 321 assertTrue("Posting a buffer failed", 322 nativeSurfaceControlPostBuffer(mNativeSurfaceControl, mColor)); 323 } 324 } 325 getLastBufferPostTime()326 public long getLastBufferPostTime() { 327 assertTrue("No buffer posted yet", mLastBufferPostTimeValid); 328 return mLastBufferPostTime; 329 } 330 release()331 public void release() { 332 if (mSurface != null) { 333 mSurface.release(); 334 mSurface = null; 335 } 336 if (mSurfaceControl != null) { 337 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 338 transaction.reparent(mSurfaceControl, null).apply(); 339 } 340 mSurfaceControl.release(); 341 mSurfaceControl = null; 342 } 343 if (mNativeSurfaceControl != 0) { 344 nativeSurfaceControlDestroy(mNativeSurfaceControl); 345 mNativeSurfaceControl = 0; 346 } 347 } 348 349 @Override finalize()350 protected void finalize() throws Throwable { 351 try { 352 release(); 353 } finally { 354 super.finalize(); 355 } 356 } 357 } 358 frameRateCompatibilityToString(int compatibility)359 private static String frameRateCompatibilityToString(int compatibility) { 360 switch (compatibility) { 361 case Surface.FRAME_RATE_COMPATIBILITY_DEFAULT: 362 return "default"; 363 case Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE: 364 return "fixed_source"; 365 default: 366 return "invalid(" + compatibility + ")"; 367 } 368 } 369 370 @Override onCreate(Bundle savedInstanceState)371 protected void onCreate(Bundle savedInstanceState) { 372 super.onCreate(savedInstanceState); 373 synchronized (mLock) { 374 mDisplayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE); 375 Display.Mode mode = getDisplay().getMode(); 376 mDeviceFrameRate = mode.getRefreshRate(); 377 // Insert the initial mode so we have the full display mode history. 378 mModeChangedEvents.add(mode); 379 mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); 380 mSurfaceView = new SurfaceView(this); 381 mSurfaceView.setWillNotDraw(false); 382 mSurfaceView.setZOrderOnTop(true); 383 setContentView(mSurfaceView, 384 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 385 ViewGroup.LayoutParams.MATCH_PARENT)); 386 mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); 387 } 388 } 389 390 @Override onDestroy()391 protected void onDestroy() { 392 super.onDestroy(); 393 mDisplayManager.unregisterDisplayListener(mDisplayListener); 394 synchronized (mLock) { 395 mActivityState = ActivityState.DESTROYED; 396 mLock.notify(); 397 } 398 } 399 400 @Override onPause()401 public void onPause() { 402 super.onPause(); 403 synchronized (mLock) { 404 mActivityState = ActivityState.PAUSED; 405 mLock.notify(); 406 } 407 } 408 409 @Override onResume()410 public void onResume() { 411 super.onResume(); 412 synchronized (mLock) { 413 mActivityState = ActivityState.RUNNING; 414 mLock.notify(); 415 } 416 } 417 418 // Returns the refresh rates with the same resolution as "mode". getRefreshRates(Display.Mode mode, Display display)419 private ArrayList<Float> getRefreshRates(Display.Mode mode, Display display) { 420 Display.Mode[] modes = display.getSupportedModes(); 421 ArrayList<Float> frameRates = new ArrayList<>(); 422 for (Display.Mode supportedMode : modes) { 423 if (hasSameResolution(supportedMode, mode)) { 424 frameRates.add(supportedMode.getRefreshRate()); 425 } 426 } 427 Collections.sort(frameRates); 428 ArrayList<Float> uniqueFrameRates = new ArrayList<>(); 429 for (float frameRate : frameRates) { 430 if (uniqueFrameRates.isEmpty() 431 || frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1) 432 >= FRAME_RATE_TOLERANCE_STRICT) { 433 uniqueFrameRates.add(frameRate); 434 } 435 } 436 return uniqueFrameRates; 437 } 438 getSeamedRefreshRates(Display.Mode mode, Display display)439 private List<Float> getSeamedRefreshRates(Display.Mode mode, Display display) { 440 List<Float> seamedRefreshRates = new ArrayList<>(); 441 Display.Mode[] modes = display.getSupportedModes(); 442 for (Display.Mode otherMode : modes) { 443 if (hasSameResolution(mode, otherMode) 444 && !DisplayUtil.isModeSwitchSeamless(mode, otherMode)) { 445 seamedRefreshRates.add(otherMode.getRefreshRate()); 446 } 447 } 448 return seamedRefreshRates; 449 } 450 hasSameResolution(Display.Mode mode1, Display.Mode mode2)451 private boolean hasSameResolution(Display.Mode mode1, Display.Mode mode2) { 452 return mode1.getPhysicalHeight() == mode2.getPhysicalHeight() 453 && mode1.getPhysicalWidth() == mode2.getPhysicalWidth(); 454 } 455 isFrameRateMultiple( float higherFrameRate, float lowerFrameRate, float tolerance)456 private boolean isFrameRateMultiple( 457 float higherFrameRate, float lowerFrameRate, float tolerance) { 458 float multiple = higherFrameRate / lowerFrameRate; 459 int roundedMultiple = Math.round(multiple); 460 return roundedMultiple > 0 461 && Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate) <= tolerance; 462 } 463 464 // Returns two device-supported frame rates that aren't multiples of each other, or null if no 465 // such incompatible frame rates are available. This is useful for testing behavior where we 466 // have layers with conflicting frame rates. getIncompatibleFrameRates(Display display)467 private float[] getIncompatibleFrameRates(Display display) { 468 ArrayList<Float> frameRates = getRefreshRates(display.getMode(), display); 469 for (int i = 0; i < frameRates.size(); i++) { 470 for (int j = i + 1; j < frameRates.size(); j++) { 471 if (!isFrameRateMultiple(Math.max(frameRates.get(i), frameRates.get(j)), 472 Math.min(frameRates.get(i), frameRates.get(j)), 473 FRAME_RATE_TOLERANCE_RELAXED)) { 474 return new float[] {frameRates.get(i), frameRates.get(j)}; 475 } 476 } 477 } 478 return null; 479 } 480 481 // Waits until our SurfaceHolder has a surface and the activity is resumed. waitForPreconditions()482 private void waitForPreconditions() throws InterruptedException { 483 assertNotSame("Activity was unexpectedly destroyed", mActivityState, 484 ActivityState.DESTROYED); 485 if (mSurface == null || mActivityState != ActivityState.RUNNING) { 486 Log.i(TAG, 487 String.format( 488 "Waiting for preconditions. Have surface? %b. Activity resumed? %b.", 489 mSurface != null, mActivityState == ActivityState.RUNNING)); 490 } 491 long nowNanos = System.nanoTime(); 492 long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L; 493 while (mSurface == null || mActivityState != ActivityState.RUNNING) { 494 long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; 495 assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b." 496 + " Activity resumed? %b.", 497 mSurface != null, mActivityState == ActivityState.RUNNING), 498 timeRemainingMillis > 0); 499 mLock.wait(timeRemainingMillis); 500 assertNotSame("Activity was unexpectedly destroyed", mActivityState, 501 ActivityState.DESTROYED); 502 nowNanos = System.nanoTime(); 503 } 504 // Make sure any previous mode changes are completed. 505 waitForStableFrameRate(); 506 } 507 508 // Returns true if we encounter a precondition violation, false otherwise. waitForPreconditionViolation()509 private boolean waitForPreconditionViolation() throws InterruptedException { 510 assertNotSame("Activity was unexpectedly destroyed", mActivityState, 511 ActivityState.DESTROYED); 512 long nowNanos = System.nanoTime(); 513 long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L; 514 while (mSurface != null && mActivityState == ActivityState.RUNNING) { 515 long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; 516 if (timeRemainingMillis <= 0) { 517 break; 518 } 519 mLock.wait(timeRemainingMillis); 520 assertNotSame("Activity was unexpectedly destroyed", mActivityState, 521 ActivityState.DESTROYED); 522 nowNanos = System.nanoTime(); 523 } 524 return mSurface == null || mActivityState != ActivityState.RUNNING; 525 } 526 verifyPreconditions()527 private void verifyPreconditions() { 528 if (mSurface == null || mActivityState != ActivityState.RUNNING) { 529 throw new PreconditionViolatedException(); 530 } 531 } 532 533 // Returns true if we reached waitUntilNanos, false if some other event occurred. waitForEvents(long waitUntilNanos, TestSurface[] surfaces)534 private boolean waitForEvents(long waitUntilNanos, TestSurface[] surfaces) 535 throws InterruptedException { 536 int numModeChangedEvents = mModeChangedEvents.size(); 537 long nowNanos = System.nanoTime(); 538 while (nowNanos < waitUntilNanos) { 539 long surfacePostTime = Long.MAX_VALUE; 540 for (TestSurface surface : surfaces) { 541 surfacePostTime = Math.min(surfacePostTime, 542 surface.getLastBufferPostTime() 543 + (POST_BUFFER_INTERVAL_MILLIS * 1_000_000L)); 544 } 545 long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos; 546 long timeoutMs = timeoutNs / 1_000_000L; 547 int remainderNs = (int) (timeoutNs % 1_000_000L); 548 // Don't call wait(0, 0) - it blocks indefinitely. 549 if (timeoutMs > 0 || remainderNs > 0) { 550 mLock.wait(timeoutMs, remainderNs); 551 } 552 nowNanos = System.nanoTime(); 553 verifyPreconditions(); 554 if (mModeChangedEvents.size() > numModeChangedEvents) { 555 return false; 556 } 557 if (nowNanos >= surfacePostTime) { 558 for (TestSurface surface : surfaces) { 559 surface.postBuffer(); 560 } 561 } 562 } 563 return true; 564 } 565 waitForStableFrameRate(TestSurface... surfaces)566 private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException { 567 verifyCompatibleAndStableFrameRate(0, FRAME_RATE_TOLERANCE_STRICT, surfaces); 568 } 569 570 // Set expectedFrameRate to 0.0 to verify only stable frame rate. verifyCompatibleAndStableFrameRate(float expectedFrameRate, float tolerance, TestSurface... surfaces)571 private void verifyCompatibleAndStableFrameRate(float expectedFrameRate, float tolerance, 572 TestSurface... surfaces) throws InterruptedException { 573 Log.i(TAG, "Verifying compatible and stable frame rate"); 574 long nowNanos = System.nanoTime(); 575 long gracePeriodEndTimeNanos = 576 nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L; 577 while (true) { 578 if (expectedFrameRate > tolerance) { // expectedFrameRate > 0 579 // Wait until we switch to a compatible frame rate. 580 while (!isFrameRateMultiple(mDeviceFrameRate, expectedFrameRate, tolerance) 581 && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) { 582 // Empty 583 } 584 nowNanos = System.nanoTime(); 585 if (nowNanos >= gracePeriodEndTimeNanos) { 586 throw new FrameRateTimeoutException(expectedFrameRate, mDeviceFrameRate); 587 } 588 } 589 590 // We've switched to a compatible frame rate. Now wait for a while to see if we stay at 591 // that frame rate. 592 long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_SECONDS * 1_000_000_000L; 593 while (endTimeNanos > nowNanos) { 594 int numModeChangedEvents = mModeChangedEvents.size(); 595 if (waitForEvents(endTimeNanos, surfaces)) { 596 Log.i(TAG, String.format("Stable frame rate %.2f verified", mDeviceFrameRate)); 597 return; 598 } 599 nowNanos = System.nanoTime(); 600 if (mModeChangedEvents.size() > numModeChangedEvents) { 601 break; 602 } 603 } 604 } 605 } 606 verifyModeSwitchesDontChangeResolution(int fromId, int toId)607 private void verifyModeSwitchesDontChangeResolution(int fromId, int toId) { 608 assertTrue(fromId <= toId); 609 for (int eventId = fromId; eventId < toId; eventId++) { 610 Display.Mode fromMode = mModeChangedEvents.get(eventId - 1); 611 Display.Mode toMode = mModeChangedEvents.get(eventId); 612 assertTrue("Resolution change was not expected, but there was such from " 613 + fromMode + " to " + toMode + ".", hasSameResolution(fromMode, toMode)); 614 } 615 } 616 verifyModeSwitchesAreSeamless(int fromId, int toId)617 private void verifyModeSwitchesAreSeamless(int fromId, int toId) { 618 assertTrue(fromId <= toId); 619 for (int eventId = fromId; eventId < toId; eventId++) { 620 Display.Mode fromMode = mModeChangedEvents.get(eventId - 1); 621 Display.Mode toMode = mModeChangedEvents.get(eventId); 622 assertTrue("Non-seamless mode switch was not expected, but there was a " 623 + "non-seamless switch from from " + fromMode + " to " + toMode + ".", 624 DisplayUtil.isModeSwitchSeamless(fromMode, toMode)); 625 } 626 } 627 628 // Unfortunately, we can't just use Consumer<Api> for this, because we need to declare that it 629 // throws InterruptedException. 630 private interface TestInterface { run(Api api)631 void run(Api api) throws InterruptedException; 632 } 633 634 private interface OneSurfaceTestInterface { run(TestSurface surface)635 void run(TestSurface surface) throws InterruptedException; 636 } 637 638 // Runs the given test for each api, waiting for the preconditions to be satisfied before 639 // running the test. Includes retry logic when the test fails because the preconditions are 640 // violated. E.g. if we lose the SurfaceHolder's surface, or the activity is paused/resumed, 641 // we'll retry the test. The activity being intermittently paused/resumed has been observed to 642 // cause test failures in practice. runTestsWithPreconditions(TestInterface test, String testName)643 private void runTestsWithPreconditions(TestInterface test, String testName) 644 throws InterruptedException { 645 synchronized (mLock) { 646 for (Api api : Api.values()) { 647 Log.i(TAG, String.format("Testing %s %s", api, testName)); 648 int attempts = 0; 649 boolean testPassed = false; 650 try { 651 while (!testPassed) { 652 waitForPreconditions(); 653 try { 654 test.run(api); 655 testPassed = true; 656 } catch (PreconditionViolatedException exc) { 657 // The logic below will retry if we're below max attempts. 658 } catch (FrameRateTimeoutException exc) { 659 StringWriter stringWriter = new StringWriter(); 660 PrintWriter printWriter = new PrintWriter(stringWriter); 661 exc.printStackTrace(printWriter); 662 String stackTrace = stringWriter.toString(); 663 664 // Sometimes we get a test timeout failure before we get the 665 // notification that the activity was paused, and it was the pause that 666 // caused the timeout failure. Wait for a bit to see if we get notified 667 // of a precondition violation, and if so, retry the test. Otherwise 668 // fail. 669 assertTrue( 670 String.format( 671 "Timed out waiting for a stable and compatible frame" 672 + " rate. expected=%.2f received=%.2f." 673 + " Stack trace: " + stackTrace, 674 exc.expectedFrameRate, exc.deviceFrameRate), 675 waitForPreconditionViolation()); 676 } 677 678 if (!testPassed) { 679 Log.i(TAG, 680 String.format("Preconditions violated while running the test." 681 + " Have surface? %b. Activity resumed? %b.", 682 mSurface != null, 683 mActivityState == ActivityState.RUNNING)); 684 attempts++; 685 assertTrue(String.format( 686 "Exceeded %d precondition wait attempts. Giving up.", 687 PRECONDITION_WAIT_MAX_ATTEMPTS), 688 attempts < PRECONDITION_WAIT_MAX_ATTEMPTS); 689 } 690 } 691 } finally { 692 String passFailMessage = String.format( 693 "%s %s %s", testPassed ? "Passed" : "Failed", api, testName); 694 if (testPassed) { 695 Log.i(TAG, passFailMessage); 696 } else { 697 Log.e(TAG, passFailMessage); 698 } 699 } 700 } 701 } 702 } 703 704 public void testExactFrameRateMatch(int changeFrameRateStrategy) throws InterruptedException { 705 String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS 706 ? "seamless" : "always"; 707 runTestsWithPreconditions(api -> testExactFrameRateMatch(api, changeFrameRateStrategy), 708 type + " exact frame rate match"); 709 } 710 testClearFrameRate()711 public void testClearFrameRate() throws InterruptedException { 712 runTestsWithPreconditions(this::testClearFrameRate, "clear frame rate"); 713 } 714 testExactFrameRateMatch(Api api, int changeFrameRateStrategy)715 private void testExactFrameRateMatch(Api api, int changeFrameRateStrategy) 716 throws InterruptedException { 717 runOneSurfaceTest(api, (TestSurface surface) -> { 718 Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 719 Display.Mode currentMode = display.getMode(); 720 721 if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) { 722 // Seamless rates should be seamlessly achieved with no resolution changes. 723 List<Float> seamlessRefreshRates = 724 Floats.asList(currentMode.getAlternativeRefreshRates()); 725 for (float frameRate : seamlessRefreshRates) { 726 int initialNumEvents = mModeChangedEvents.size(); 727 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 728 Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); 729 verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_RELAXED, 730 surface); 731 verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size()); 732 verifyModeSwitchesDontChangeResolution(initialNumEvents, 733 mModeChangedEvents.size()); 734 } 735 // Reset to default 736 surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 737 Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); 738 // Wait for potential mode switches 739 waitForStableFrameRate(surface); 740 currentMode = display.getMode(); 741 742 // Seamed rates should never generate a seamed switch. 743 List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display); 744 for (float frameRate : seamedRefreshRates) { 745 int initialNumEvents = mModeChangedEvents.size(); 746 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 747 Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); 748 // Mode switch can occur, since we could potentially switch to a multiple 749 // that happens to be seamless. 750 verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size()); 751 } 752 } else if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ALWAYS) { 753 // All rates should be seamfully achieved with no resolution changes. 754 List<Float> allRefreshRates = getRefreshRates(currentMode, display); 755 for (float frameRate : allRefreshRates) { 756 int initialNumEvents = mModeChangedEvents.size(); 757 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 758 Surface.CHANGE_FRAME_RATE_ALWAYS); 759 verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_RELAXED, 760 surface); 761 verifyModeSwitchesDontChangeResolution(initialNumEvents, 762 mModeChangedEvents.size()); 763 } 764 } else { 765 Log.e(TAG, "Invalid changeFrameRateStrategy = " + changeFrameRateStrategy); 766 } 767 }); 768 } 769 modeSwitchesToString(int fromId, int toId)770 private String modeSwitchesToString(int fromId, int toId) { 771 assertTrue(fromId <= toId); 772 String string = ""; 773 for (int eventId = fromId; eventId < toId; eventId++) { 774 Display.Mode fromMode = mModeChangedEvents.get(eventId - 1); 775 Display.Mode toMode = mModeChangedEvents.get(eventId); 776 string += fromMode + " -> " + toMode + "; "; 777 } 778 return string; 779 } 780 testFixedSource(Api api, int changeFrameRateStrategy)781 private void testFixedSource(Api api, int changeFrameRateStrategy) throws InterruptedException { 782 Display display = getDisplay(); 783 float[] incompatibleFrameRates = getIncompatibleFrameRates(display); 784 if (incompatibleFrameRates == null) { 785 Log.i(TAG, "No incompatible frame rates to use for testing fixed_source behavior"); 786 return; 787 } 788 789 float frameRateA = incompatibleFrameRates[0]; 790 float frameRateB = incompatibleFrameRates[1]; 791 Log.i(TAG, 792 String.format("Testing with incompatible frame rates: surfaceA=%.2f surfaceB=%.2f", 793 frameRateA, frameRateB)); 794 TestSurface surfaceA = null; 795 TestSurface surfaceB = null; 796 797 try { 798 int width = mSurfaceView.getHolder().getSurfaceFrame().width(); 799 int height = mSurfaceView.getHolder().getSurfaceFrame().height() / 2; 800 Rect destFrameA = new Rect(/*left=*/0, /*top=*/0, /*right=*/width, /*bottom=*/height); 801 surfaceA = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, "surfaceA", 802 destFrameA, /*visible=*/true, Color.RED); 803 Rect destFrameB = new Rect( 804 /*left=*/0, /*top=*/height, /*right=*/width, /*bottom=*/height * 2); 805 surfaceB = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, "surfaceB", 806 destFrameB, /*visible=*/false, Color.GREEN); 807 808 int initialNumEvents = mModeChangedEvents.size(); 809 surfaceA.setFrameRate(frameRateA, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 810 changeFrameRateStrategy); 811 surfaceB.setFrameRate(frameRateB, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 812 changeFrameRateStrategy); 813 814 if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) { 815 verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size()); 816 } else { 817 verifyCompatibleAndStableFrameRate(frameRateA, FRAME_RATE_TOLERANCE_STRICT, 818 surfaceA, surfaceB); 819 } 820 821 verifyModeSwitchesDontChangeResolution(initialNumEvents, 822 mModeChangedEvents.size()); 823 initialNumEvents = mModeChangedEvents.size(); 824 825 surfaceB.setVisibility(true); 826 827 if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) { 828 verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size()); 829 } else { 830 verifyCompatibleAndStableFrameRate(frameRateB, FRAME_RATE_TOLERANCE_STRICT, 831 surfaceA, surfaceB); 832 } 833 verifyModeSwitchesDontChangeResolution(initialNumEvents, 834 mModeChangedEvents.size()); 835 } finally { 836 if (surfaceA != null) { 837 surfaceA.release(); 838 } 839 if (surfaceB != null) { 840 surfaceB.release(); 841 } 842 } 843 } 844 testFixedSource(int changeFrameRateStrategy)845 public void testFixedSource(int changeFrameRateStrategy) throws InterruptedException { 846 String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS 847 ? "seamless" : "always"; 848 runTestsWithPreconditions(api -> testFixedSource(api, changeFrameRateStrategy), 849 type + " fixed source behavior"); 850 } 851 testInvalidParams(Api api)852 private void testInvalidParams(Api api) { 853 TestSurface surface = null; 854 final int changeStrategy = Surface.CHANGE_FRAME_RATE_ALWAYS; 855 try { 856 surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, 857 "testSurface", mSurfaceView.getHolder().getSurfaceFrame(), 858 /*visible=*/true, Color.RED); 859 int initialNumEvents = mModeChangedEvents.size(); 860 surface.setInvalidFrameRate(-100.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 861 changeStrategy); 862 assertEquals(initialNumEvents, mModeChangedEvents.size()); 863 surface.setInvalidFrameRate(Float.POSITIVE_INFINITY, 864 Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, changeStrategy); 865 assertEquals(initialNumEvents, mModeChangedEvents.size()); 866 surface.setInvalidFrameRate(Float.NaN, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 867 changeStrategy); 868 assertEquals(initialNumEvents, mModeChangedEvents.size()); 869 surface.setInvalidFrameRate(0.f, -10, changeStrategy); 870 assertEquals(initialNumEvents, mModeChangedEvents.size()); 871 surface.setInvalidFrameRate(0.f, 50, changeStrategy); 872 assertEquals(initialNumEvents, mModeChangedEvents.size()); 873 } finally { 874 if (surface != null) { 875 surface.release(); 876 } 877 } 878 } 879 testInvalidParams()880 public void testInvalidParams() throws InterruptedException { 881 runTestsWithPreconditions(this::testInvalidParams, "invalid params behavior"); 882 } 883 runOneSurfaceTest(Api api, OneSurfaceTestInterface test)884 private void runOneSurfaceTest(Api api, OneSurfaceTestInterface test) 885 throws InterruptedException { 886 TestSurface surface = null; 887 try { 888 surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, 889 "testSurface", mSurfaceView.getHolder().getSurfaceFrame(), 890 /*visible=*/true, Color.RED); 891 892 test.run(surface); 893 } finally { 894 if (surface != null) { 895 surface.release(); 896 } 897 } 898 } 899 testMatchContentFramerate_None(Api api)900 private void testMatchContentFramerate_None(Api api) throws InterruptedException { 901 runOneSurfaceTest(api, (TestSurface surface) -> { 902 Display display = getDisplay(); 903 Display.Mode currentMode = display.getMode(); 904 List<Float> frameRates = getRefreshRates(currentMode, display); 905 906 for (float frameRate : frameRates) { 907 int initialNumEvents = mModeChangedEvents.size(); 908 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 909 Surface.CHANGE_FRAME_RATE_ALWAYS); 910 911 assertEquals("Mode switches are not expected but these were detected " 912 + modeSwitchesToString(initialNumEvents, mModeChangedEvents.size()), 913 mModeChangedEvents.size(), initialNumEvents); 914 } 915 }); 916 } 917 testMatchContentFramerate_None()918 public void testMatchContentFramerate_None() throws InterruptedException { 919 runTestsWithPreconditions(this::testMatchContentFramerate_None, 920 "testMatchContentFramerate_None"); 921 } 922 testMatchContentFramerate_Auto(Api api)923 private void testMatchContentFramerate_Auto(Api api) 924 throws InterruptedException { 925 runOneSurfaceTest(api, (TestSurface surface) -> { 926 Display display = getDisplay(); 927 Display.Mode currentMode = display.getMode(); 928 List<Float> frameRatesToTest = Floats.asList(currentMode.getAlternativeRefreshRates()); 929 930 for (float frameRate : frameRatesToTest) { 931 int initialNumEvents = mModeChangedEvents.size(); 932 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 933 Surface.CHANGE_FRAME_RATE_ALWAYS); 934 935 verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_STRICT, surface); 936 verifyModeSwitchesDontChangeResolution(initialNumEvents, 937 mModeChangedEvents.size()); 938 } 939 940 // Reset to default 941 surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 942 Surface.CHANGE_FRAME_RATE_ALWAYS); 943 944 // Wait for potential mode switches. 945 waitForStableFrameRate(surface); 946 947 currentMode = display.getMode(); 948 List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display); 949 950 for (float frameRate : seamedRefreshRates) { 951 int initialNumEvents = mModeChangedEvents.size(); 952 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 953 Surface.CHANGE_FRAME_RATE_ALWAYS); 954 955 // Mode switches may have occurred, make sure they were all seamless. 956 verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size()); 957 verifyModeSwitchesDontChangeResolution(initialNumEvents, 958 mModeChangedEvents.size()); 959 } 960 }); 961 } 962 testMatchContentFramerate_Auto()963 public void testMatchContentFramerate_Auto() throws InterruptedException { 964 runTestsWithPreconditions(this::testMatchContentFramerate_Auto, 965 "testMatchContentFramerate_Auto"); 966 } 967 testMatchContentFramerate_Always(Api api)968 private void testMatchContentFramerate_Always(Api api) throws InterruptedException { 969 runOneSurfaceTest(api, (TestSurface surface) -> { 970 Display display = getDisplay(); 971 List<Float> frameRates = getRefreshRates(display.getMode(), display); 972 for (float frameRate : frameRates) { 973 int initialNumEvents = mModeChangedEvents.size(); 974 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, 975 Surface.CHANGE_FRAME_RATE_ALWAYS); 976 977 verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_STRICT, surface); 978 verifyModeSwitchesDontChangeResolution(initialNumEvents, 979 mModeChangedEvents.size()); 980 } 981 }); 982 } 983 testClearFrameRate(Api api)984 private void testClearFrameRate(Api api) throws InterruptedException { 985 Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); 986 Display.Mode initialMode = display.getMode(); 987 988 int width = mSurfaceView.getHolder().getSurfaceFrame().width(); 989 int height = mSurfaceView.getHolder().getSurfaceFrame().height() / 2; 990 Rect destFrameA = new Rect(/*left=*/0, /*top=*/0, /*right=*/width, /*bottom=*/height); 991 TestSurface surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, 992 "surface", destFrameA, /*visible=*/true, Color.RED); 993 994 // Verify clear frame rate if set frame rate is seamless 995 List<Float> seamlessRefreshRates = 996 Floats.asList(initialMode.getAlternativeRefreshRates()); 997 verifyClearFrameRate(surface, seamlessRefreshRates, initialMode.getRefreshRate(), 998 Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS); 999 1000 // Verify clear frame rate if set frame rate is non-seamless 1001 List<Float> seamedRefreshRates = getSeamedRefreshRates(initialMode, display); 1002 verifyClearFrameRate(surface, seamedRefreshRates, initialMode.getRefreshRate(), 1003 Surface.CHANGE_FRAME_RATE_ALWAYS); 1004 } 1005 verifyClearFrameRate(TestSurface surface, List<Float> refreshRates, float initialRefreshRate, int changeFrameRateStrategy)1006 private void verifyClearFrameRate(TestSurface surface, List<Float> refreshRates, 1007 float initialRefreshRate, int changeFrameRateStrategy) throws InterruptedException { 1008 for (float frameRate : refreshRates) { 1009 if (initialRefreshRate != frameRate) { 1010 surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, 1011 changeFrameRateStrategy); 1012 verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_RELAXED, 1013 surface); 1014 1015 // Clear the frame-rate 1016 surface.clearFrameRate(); 1017 waitForStableFrameRate(surface); 1018 break; 1019 } 1020 } 1021 } 1022 testMatchContentFramerate_Always()1023 public void testMatchContentFramerate_Always() throws InterruptedException { 1024 runTestsWithPreconditions(this::testMatchContentFramerate_Always, 1025 "testMatchContentFramerate_Always"); 1026 } 1027 1028 private static native int nativeWindowSetFrameRate( 1029 Surface surface, float frameRate, int compatibility, int changeFrameRateStrategy); 1030 private static native long nativeSurfaceControlCreate( 1031 Surface parentSurface, String name, int left, int top, int right, int bottom); 1032 private static native void nativeSurfaceControlDestroy(long surfaceControl); 1033 private static native void nativeSurfaceControlSetFrameRate( 1034 long surfaceControl, float frameRate, int compatibility, int changeFrameRateStrategy); 1035 private static native void nativeSurfaceControlSetVisibility( 1036 long surfaceControl, boolean visible); 1037 private static native boolean nativeSurfaceControlPostBuffer(long surfaceControl, int color); 1038 private static native int nativeWindowClearFrameRate(Surface surface); 1039 private static native void nativeSurfaceControlClearFrameRate(long surfaceControl); 1040 } 1041