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