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