blob: 607d7c1bf2a1857d69d870553787576e0dc6509b [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.cts;
import static android.system.OsConstants.EINVAL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import com.android.compatibility.common.util.DisplayUtil;
import com.google.common.primitives.Floats;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An Activity to help with frame rate testing.
*/
public class FrameRateCtsActivity extends Activity {
static {
System.loadLibrary("ctsgraphics_jni");
}
private static final String TAG = "FrameRateCtsActivity";
private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS = 2;
private static final long STABLE_FRAME_RATE_WAIT_SECONDS = 1;
private static final long POST_BUFFER_INTERVAL_MILLIS = 500;
private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5;
private static final long PRECONDITION_WAIT_TIMEOUT_SECONDS = 20;
private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS = 3;
private static final float FRAME_RATE_TOLERANCE_STRICT = 0.01f;
// Tolerance which doesn't differentiate between the fractional refresh rate pairs, e.g.
// 59.94 and 60 will be considered the same refresh rate.
// Use this tolerance to verify the refresh rate after calling setFrameRate with
// {@Surface.FRAME_RATE_COMPATIBILITY_DEFAULT}.
private static final float FRAME_RATE_TOLERANCE_RELAXED = 0.1f;
private DisplayManager mDisplayManager;
private SurfaceView mSurfaceView;
private Handler mHandler = new Handler(Looper.getMainLooper());
private final Object mLock = new Object();
private Surface mSurface = null;
private float mDeviceFrameRate;
private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents();
private enum ActivityState { RUNNING, PAUSED, DESTROYED }
private ActivityState mActivityState;
SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
synchronized (mLock) {
mSurface = holder.getSurface();
mLock.notify();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
synchronized (mLock) {
mSurface = null;
mLock.notify();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
};
DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {}
@Override
public void onDisplayChanged(int displayId) {
if (displayId != Display.DEFAULT_DISPLAY) {
return;
}
synchronized (mLock) {
Display.Mode mode = mDisplayManager.getDisplay(displayId).getMode();
mModeChangedEvents.add(mode);
float frameRate = mode.getRefreshRate();
if (frameRate != mDeviceFrameRate) {
Log.i(TAG,
String.format("Frame rate changed: %.2f --> %.2f", mDeviceFrameRate,
frameRate));
mDeviceFrameRate = frameRate;
mLock.notify();
}
}
}
@Override
public void onDisplayRemoved(int displayId) {}
};
// Wrapper around ArrayList for which the only allowed mutable operation is add().
// We use this to store all mode change events during a test. When we need to iterate over
// all mode changes during a certain operation, we use the number of events in the beginning
// and in the end. It's important to never clear or modify the elements in this list hence the
// wrapper.
private static class ModeChangedEvents {
private List<Display.Mode> mEvents = new ArrayList<>();
public void add(Display.Mode mode) {
mEvents.add(mode);
}
public Display.Mode get(int i) {
return mEvents.get(i);
}
public int size() {
return mEvents.size();
}
}
private static class PreconditionViolatedException extends RuntimeException {
PreconditionViolatedException() {}
}
private static class FrameRateTimeoutException extends RuntimeException {
FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate) {
this.expectedFrameRate = expectedFrameRate;
this.deviceFrameRate = deviceFrameRate;
}
public float expectedFrameRate;
public float deviceFrameRate;
}
public enum Api {
SURFACE("Surface"),
ANATIVE_WINDOW("ANativeWindow"),
SURFACE_CONTROL("SurfaceControl"),
NATIVE_SURFACE_CONTROL("ASurfaceControl");
private final String mName;
Api(String name) {
mName = name;
}
public String toString() {
return mName;
}
}
private static class TestSurface {
private Api mApi;
private String mName;
private SurfaceControl mSurfaceControl;
private Surface mSurface;
private long mNativeSurfaceControl;
private int mColor;
private boolean mLastBufferPostTimeValid;
private long mLastBufferPostTime;
TestSurface(Api api, SurfaceControl parentSurfaceControl, Surface parentSurface,
String name, Rect destFrame, boolean visible, int color) {
mApi = api;
mName = name;
mColor = color;
if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) {
assertNotNull("No parent surface", parentSurfaceControl);
mSurfaceControl = new SurfaceControl.Builder()
.setParent(parentSurfaceControl)
.setName(mName)
.setBufferSize(destFrame.right - destFrame.left,
destFrame.bottom - destFrame.top)
.build();
try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
transaction.setGeometry(mSurfaceControl, null, destFrame, Surface.ROTATION_0)
.apply();
}
mSurface = new Surface(mSurfaceControl);
} else if (mApi == Api.NATIVE_SURFACE_CONTROL) {
assertNotNull("No parent surface", parentSurface);
mNativeSurfaceControl = nativeSurfaceControlCreate(parentSurface, mName,
destFrame.left, destFrame.top, destFrame.right, destFrame.bottom);
assertTrue("Failed to create a native SurfaceControl", mNativeSurfaceControl != 0);
}
setVisibility(visible);
postBuffer();
}
public int setFrameRate(float frameRate, int compatibility, int changeFrameRateStrategy) {
Log.i(TAG,
String.format("Setting frame rate for %s: fps=%.2f compatibility=%s", mName,
frameRate, frameRateCompatibilityToString(compatibility)));
int rc = 0;
if (mApi == Api.SURFACE) {
if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
mSurface.setFrameRate(frameRate, compatibility);
} else {
mSurface.setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
}
} else if (mApi == Api.ANATIVE_WINDOW) {
rc = nativeWindowSetFrameRate(mSurface, frameRate, compatibility,
changeFrameRateStrategy);
} else if (mApi == Api.SURFACE_CONTROL) {
try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
transaction
.setFrameRate(mSurfaceControl, frameRate, compatibility);
} else {
transaction
.setFrameRate(mSurfaceControl, frameRate, compatibility,
changeFrameRateStrategy);
}
transaction.apply();
}
} else if (mApi == Api.NATIVE_SURFACE_CONTROL) {
nativeSurfaceControlSetFrameRate(mNativeSurfaceControl, frameRate, compatibility,
changeFrameRateStrategy);
}
return rc;
}
public void setInvalidFrameRate(float frameRate, int compatibility,
int changeFrameRateStrategy) {
if (mApi == Api.SURFACE) {
boolean caughtIllegalArgException = false;
try {
setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
} catch (IllegalArgumentException exc) {
caughtIllegalArgException = true;
}
assertTrue("Expected an IllegalArgumentException from invalid call to"
+ " Surface.setFrameRate()",
caughtIllegalArgException);
} else {
int rc = setFrameRate(frameRate, compatibility, changeFrameRateStrategy);
if (mApi == Api.ANATIVE_WINDOW) {
assertEquals("Expected -EINVAL return value from invalid call to"
+ " ANativeWindow_setFrameRate()", rc, -EINVAL);
}
}
}
public void setVisibility(boolean visible) {
Log.i(TAG,
String.format("Setting visibility for %s: %s", mName,
visible ? "visible" : "hidden"));
if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) {
try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
transaction.setVisibility(mSurfaceControl, visible).apply();
}
} else if (mApi == Api.NATIVE_SURFACE_CONTROL) {
nativeSurfaceControlSetVisibility(mNativeSurfaceControl, visible);
}
}
public void postBuffer() {
mLastBufferPostTimeValid = true;
mLastBufferPostTime = System.nanoTime();
if (mApi == Api.SURFACE || mApi == Api.ANATIVE_WINDOW || mApi == Api.SURFACE_CONTROL) {
Canvas canvas = mSurface.lockHardwareCanvas();
canvas.drawColor(mColor);
mSurface.unlockCanvasAndPost(canvas);
} else if (mApi == Api.NATIVE_SURFACE_CONTROL) {
assertTrue("Posting a buffer failed",
nativeSurfaceControlPostBuffer(mNativeSurfaceControl, mColor));
}
}
public long getLastBufferPostTime() {
assertTrue("No buffer posted yet", mLastBufferPostTimeValid);
return mLastBufferPostTime;
}
public void release() {
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
if (mSurfaceControl != null) {
try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) {
transaction.reparent(mSurfaceControl, null).apply();
}
mSurfaceControl.release();
mSurfaceControl = null;
}
if (mNativeSurfaceControl != 0) {
nativeSurfaceControlDestroy(mNativeSurfaceControl);
mNativeSurfaceControl = 0;
}
}
@Override
protected void finalize() throws Throwable {
try {
release();
} finally {
super.finalize();
}
}
}
private static String frameRateCompatibilityToString(int compatibility) {
switch (compatibility) {
case Surface.FRAME_RATE_COMPATIBILITY_DEFAULT:
return "default";
case Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE:
return "fixed_source";
default:
return "invalid(" + compatibility + ")";
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
synchronized (mLock) {
mDisplayManager = (DisplayManager) getSystemService(DISPLAY_SERVICE);
Display.Mode mode = getDisplay().getMode();
mDeviceFrameRate = mode.getRefreshRate();
// Insert the initial mode so we have the full display mode history.
mModeChangedEvents.add(mode);
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
mSurfaceView = new SurfaceView(this);
mSurfaceView.setWillNotDraw(false);
mSurfaceView.setZOrderOnTop(true);
setContentView(mSurfaceView,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mDisplayManager.unregisterDisplayListener(mDisplayListener);
synchronized (mLock) {
mActivityState = ActivityState.DESTROYED;
mLock.notify();
}
}
@Override
public void onPause() {
super.onPause();
synchronized (mLock) {
mActivityState = ActivityState.PAUSED;
mLock.notify();
}
}
@Override
public void onResume() {
super.onResume();
synchronized (mLock) {
mActivityState = ActivityState.RUNNING;
mLock.notify();
}
}
// Returns the refresh rates with the same resolution as "mode".
private ArrayList<Float> getRefreshRates(Display.Mode mode, Display display) {
Display.Mode[] modes = display.getSupportedModes();
ArrayList<Float> frameRates = new ArrayList<>();
for (Display.Mode supportedMode : modes) {
if (hasSameResolution(supportedMode, mode)) {
frameRates.add(supportedMode.getRefreshRate());
}
}
Collections.sort(frameRates);
ArrayList<Float> uniqueFrameRates = new ArrayList<>();
for (float frameRate : frameRates) {
if (uniqueFrameRates.isEmpty()
|| frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1)
>= FRAME_RATE_TOLERANCE_STRICT) {
uniqueFrameRates.add(frameRate);
}
}
return uniqueFrameRates;
}
private List<Float> getSeamedRefreshRates(Display.Mode mode, Display display) {
List<Float> seamedRefreshRates = new ArrayList<>();
Display.Mode[] modes = display.getSupportedModes();
for (Display.Mode otherMode : modes) {
if (!DisplayUtil.isModeSwitchSeamless(mode, otherMode)) {
seamedRefreshRates.add(otherMode.getRefreshRate());
}
}
return seamedRefreshRates;
}
private boolean hasSameResolution(Display.Mode mode1, Display.Mode mode2) {
return mode1.getPhysicalHeight() == mode2.getPhysicalHeight()
&& mode1.getPhysicalWidth() == mode2.getPhysicalWidth();
}
private boolean isFrameRateMultiple(
float higherFrameRate, float lowerFrameRate, float tolerance) {
float multiple = higherFrameRate / lowerFrameRate;
int roundedMultiple = Math.round(multiple);
return roundedMultiple > 0
&& Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate) <= tolerance;
}
// Returns two device-supported frame rates that aren't multiples of each other, or null if no
// such incompatible frame rates are available. This is useful for testing behavior where we
// have layers with conflicting frame rates.
private float[] getIncompatibleFrameRates(Display display) {
ArrayList<Float> frameRates = getRefreshRates(display.getMode(), display);
for (int i = 0; i < frameRates.size(); i++) {
for (int j = i + 1; j < frameRates.size(); j++) {
if (!isFrameRateMultiple(Math.max(frameRates.get(i), frameRates.get(j)),
Math.min(frameRates.get(i), frameRates.get(j)),
FRAME_RATE_TOLERANCE_RELAXED)) {
return new float[] {frameRates.get(i), frameRates.get(j)};
}
}
}
return null;
}
// Waits until our SurfaceHolder has a surface and the activity is resumed.
private void waitForPreconditions() throws InterruptedException {
assertNotSame("Activity was unexpectedly destroyed", mActivityState,
ActivityState.DESTROYED);
if (mSurface == null || mActivityState != ActivityState.RUNNING) {
Log.i(TAG,
String.format(
"Waiting for preconditions. Have surface? %b. Activity resumed? %b.",
mSurface != null, mActivityState == ActivityState.RUNNING));
}
long nowNanos = System.nanoTime();
long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L;
while (mSurface == null || mActivityState != ActivityState.RUNNING) {
long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b."
+ " Activity resumed? %b.",
mSurface != null, mActivityState == ActivityState.RUNNING),
timeRemainingMillis > 0);
mLock.wait(timeRemainingMillis);
assertNotSame("Activity was unexpectedly destroyed", mActivityState,
ActivityState.DESTROYED);
nowNanos = System.nanoTime();
}
// Make sure any previous mode changes are completed.
waitForStableFrameRate();
}
// Returns true if we encounter a precondition violation, false otherwise.
private boolean waitForPreconditionViolation() throws InterruptedException {
assertNotSame("Activity was unexpectedly destroyed", mActivityState,
ActivityState.DESTROYED);
long nowNanos = System.nanoTime();
long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L;
while (mSurface != null && mActivityState == ActivityState.RUNNING) {
long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000;
if (timeRemainingMillis <= 0) {
break;
}
mLock.wait(timeRemainingMillis);
assertNotSame("Activity was unexpectedly destroyed", mActivityState,
ActivityState.DESTROYED);
nowNanos = System.nanoTime();
}
return mSurface == null || mActivityState != ActivityState.RUNNING;
}
private void verifyPreconditions() {
if (mSurface == null || mActivityState != ActivityState.RUNNING) {
throw new PreconditionViolatedException();
}
}
// Returns true if we reached waitUntilNanos, false if some other event occurred.
private boolean waitForEvents(long waitUntilNanos, TestSurface[] surfaces)
throws InterruptedException {
int numModeChangedEvents = mModeChangedEvents.size();
long nowNanos = System.nanoTime();
while (nowNanos < waitUntilNanos) {
long surfacePostTime = Long.MAX_VALUE;
for (TestSurface surface : surfaces) {
surfacePostTime = Math.min(surfacePostTime,
surface.getLastBufferPostTime()
+ (POST_BUFFER_INTERVAL_MILLIS * 1_000_000L));
}
long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos;
long timeoutMs = timeoutNs / 1_000_000L;
int remainderNs = (int) (timeoutNs % 1_000_000L);
// Don't call wait(0, 0) - it blocks indefinitely.
if (timeoutMs > 0 || remainderNs > 0) {
mLock.wait(timeoutMs, remainderNs);
}
nowNanos = System.nanoTime();
verifyPreconditions();
if (mModeChangedEvents.size() > numModeChangedEvents) {
return false;
}
if (nowNanos >= surfacePostTime) {
for (TestSurface surface : surfaces) {
surface.postBuffer();
}
}
}
return true;
}
private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException {
verifyCompatibleAndStableFrameRate(0, FRAME_RATE_TOLERANCE_STRICT, surfaces);
}
// Set expectedFrameRate to 0.0 to verify only stable frame rate.
private void verifyCompatibleAndStableFrameRate(float expectedFrameRate, float tolerance,
TestSurface... surfaces) throws InterruptedException {
Log.i(TAG, "Verifying compatible and stable frame rate");
long nowNanos = System.nanoTime();
long gracePeriodEndTimeNanos =
nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L;
while (true) {
if (expectedFrameRate > tolerance) { // expectedFrameRate > 0
// Wait until we switch to a compatible frame rate.
while (!isFrameRateMultiple(mDeviceFrameRate, expectedFrameRate, tolerance)
&& !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
// Empty
}
nowNanos = System.nanoTime();
if (nowNanos >= gracePeriodEndTimeNanos) {
throw new FrameRateTimeoutException(expectedFrameRate, mDeviceFrameRate);
}
}
// We've switched to a compatible frame rate. Now wait for a while to see if we stay at
// that frame rate.
long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_SECONDS * 1_000_000_000L;
while (endTimeNanos > nowNanos) {
int numModeChangedEvents = mModeChangedEvents.size();
if (waitForEvents(endTimeNanos, surfaces)) {
Log.i(TAG, String.format("Stable frame rate %.2f verified", mDeviceFrameRate));
return;
}
nowNanos = System.nanoTime();
if (mModeChangedEvents.size() > numModeChangedEvents) {
break;
}
}
}
}
private void verifyModeSwitchesDontChangeResolution(int fromId, int toId) {
assertTrue(fromId <= toId);
for (int eventId = fromId; eventId < toId; eventId++) {
Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
Display.Mode toMode = mModeChangedEvents.get(eventId);
assertTrue("Resolution change was not expected, but there was such from "
+ fromMode + " to " + toMode + ".", hasSameResolution(fromMode, toMode));
}
}
private void verifyModeSwitchesAreSeamless(int fromId, int toId) {
assertTrue(fromId <= toId);
for (int eventId = fromId; eventId < toId; eventId++) {
Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
Display.Mode toMode = mModeChangedEvents.get(eventId);
assertTrue("Non-seamless mode switch was not expected, but there was a "
+ "non-seamless switch from from " + fromMode + " to " + toMode + ".",
DisplayUtil.isModeSwitchSeamless(fromMode, toMode));
}
}
// Unfortunately, we can't just use Consumer<Api> for this, because we need to declare that it
// throws InterruptedException.
private interface TestInterface {
void run(Api api) throws InterruptedException;
}
private interface OneSurfaceTestInterface {
void run(TestSurface surface) throws InterruptedException;
}
// Runs the given test for each api, waiting for the preconditions to be satisfied before
// running the test. Includes retry logic when the test fails because the preconditions are
// violated. E.g. if we lose the SurfaceHolder's surface, or the activity is paused/resumed,
// we'll retry the test. The activity being intermittently paused/resumed has been observed to
// cause test failures in practice.
private void runTestsWithPreconditions(TestInterface test, String testName)
throws InterruptedException {
synchronized (mLock) {
for (Api api : Api.values()) {
Log.i(TAG, String.format("Testing %s %s", api, testName));
int attempts = 0;
boolean testPassed = false;
try {
while (!testPassed) {
waitForPreconditions();
try {
test.run(api);
testPassed = true;
} catch (PreconditionViolatedException exc) {
// The logic below will retry if we're below max attempts.
} catch (FrameRateTimeoutException exc) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
exc.printStackTrace(printWriter);
String stackTrace = stringWriter.toString();
// Sometimes we get a test timeout failure before we get the
// notification that the activity was paused, and it was the pause that
// caused the timeout failure. Wait for a bit to see if we get notified
// of a precondition violation, and if so, retry the test. Otherwise
// fail.
assertTrue(
String.format(
"Timed out waiting for a stable and compatible frame"
+ " rate. expected=%.2f received=%.2f."
+ " Stack trace: " + stackTrace,
exc.expectedFrameRate, exc.deviceFrameRate),
waitForPreconditionViolation());
}
if (!testPassed) {
Log.i(TAG,
String.format("Preconditions violated while running the test."
+ " Have surface? %b. Activity resumed? %b.",
mSurface != null,
mActivityState == ActivityState.RUNNING));
attempts++;
assertTrue(String.format(
"Exceeded %d precondition wait attempts. Giving up.",
PRECONDITION_WAIT_MAX_ATTEMPTS),
attempts < PRECONDITION_WAIT_MAX_ATTEMPTS);
}
}
} finally {
String passFailMessage = String.format(
"%s %s %s", testPassed ? "Passed" : "Failed", api, testName);
if (testPassed) {
Log.i(TAG, passFailMessage);
} else {
Log.e(TAG, passFailMessage);
}
}
}
}
}
public void testExactFrameRateMatch(int changeFrameRateStrategy) throws InterruptedException {
String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
? "seamless" : "always";
runTestsWithPreconditions(api -> testExactFrameRateMatch(api, changeFrameRateStrategy),
type + " exact frame rate match");
}
private void testExactFrameRateMatch(Api api, int changeFrameRateStrategy)
throws InterruptedException {
runOneSurfaceTest(api, (TestSurface surface) -> {
Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
Display.Mode currentMode = display.getMode();
if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
// Seamless rates should be seamlessly achieved with no resolution changes.
List<Float> seamlessRefreshRates =
Floats.asList(currentMode.getAlternativeRefreshRates());
for (float frameRate : seamlessRefreshRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_RELAXED,
surface);
verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
}
// Reset to default
surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
// Wait for potential mode switches
waitForStableFrameRate(surface);
currentMode = display.getMode();
// Seamed rates should never generate a seamed switch.
List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display);
for (float frameRate : seamedRefreshRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
// Mode switch can occur, since we could potentially switch to a multiple
// that happens to be seamless.
verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
}
} else if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ALWAYS) {
// All rates should be seamfully achieved with no resolution changes.
List<Float> allRefreshRates = getRefreshRates(currentMode, display);
for (float frameRate : allRefreshRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
Surface.CHANGE_FRAME_RATE_ALWAYS);
verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_RELAXED,
surface);
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
}
} else {
Log.e(TAG, "Invalid changeFrameRateStrategy = " + changeFrameRateStrategy);
}
});
}
private String modeSwitchesToString(int fromId, int toId) {
assertTrue(fromId <= toId);
String string = "";
for (int eventId = fromId; eventId < toId; eventId++) {
Display.Mode fromMode = mModeChangedEvents.get(eventId - 1);
Display.Mode toMode = mModeChangedEvents.get(eventId);
string += fromMode + " -> " + toMode + "; ";
}
return string;
}
private void testFixedSource(Api api, int changeFrameRateStrategy) throws InterruptedException {
Display display = getDisplay();
float[] incompatibleFrameRates = getIncompatibleFrameRates(display);
if (incompatibleFrameRates == null) {
Log.i(TAG, "No incompatible frame rates to use for testing fixed_source behavior");
return;
}
float frameRateA = incompatibleFrameRates[0];
float frameRateB = incompatibleFrameRates[1];
Log.i(TAG,
String.format("Testing with incompatible frame rates: surfaceA=%.2f surfaceB=%.2f",
frameRateA, frameRateB));
TestSurface surfaceA = null;
TestSurface surfaceB = null;
try {
int width = mSurfaceView.getHolder().getSurfaceFrame().width();
int height = mSurfaceView.getHolder().getSurfaceFrame().height() / 2;
Rect destFrameA = new Rect(/*left=*/0, /*top=*/0, /*right=*/width, /*bottom=*/height);
surfaceA = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, "surfaceA",
destFrameA, /*visible=*/true, Color.RED);
Rect destFrameB = new Rect(
/*left=*/0, /*top=*/height, /*right=*/width, /*bottom=*/height * 2);
surfaceB = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface, "surfaceB",
destFrameB, /*visible=*/false, Color.GREEN);
int initialNumEvents = mModeChangedEvents.size();
surfaceA.setFrameRate(frameRateA, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
changeFrameRateStrategy);
surfaceB.setFrameRate(frameRateB, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
changeFrameRateStrategy);
if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
} else {
verifyCompatibleAndStableFrameRate(frameRateA, FRAME_RATE_TOLERANCE_STRICT,
surfaceA, surfaceB);
}
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
initialNumEvents = mModeChangedEvents.size();
surfaceB.setVisibility(true);
if (changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS) {
verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
} else {
verifyCompatibleAndStableFrameRate(frameRateB, FRAME_RATE_TOLERANCE_STRICT,
surfaceA, surfaceB);
}
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
} finally {
if (surfaceA != null) {
surfaceA.release();
}
if (surfaceB != null) {
surfaceB.release();
}
}
}
public void testFixedSource(int changeFrameRateStrategy) throws InterruptedException {
String type = changeFrameRateStrategy == Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
? "seamless" : "always";
runTestsWithPreconditions(api -> testFixedSource(api, changeFrameRateStrategy),
type + " fixed source behavior");
}
private void testInvalidParams(Api api) {
TestSurface surface = null;
final int changeStrategy = Surface.CHANGE_FRAME_RATE_ALWAYS;
try {
surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface,
"testSurface", mSurfaceView.getHolder().getSurfaceFrame(),
/*visible=*/true, Color.RED);
int initialNumEvents = mModeChangedEvents.size();
surface.setInvalidFrameRate(-100.f, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
changeStrategy);
assertEquals(initialNumEvents, mModeChangedEvents.size());
surface.setInvalidFrameRate(Float.POSITIVE_INFINITY,
Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, changeStrategy);
assertEquals(initialNumEvents, mModeChangedEvents.size());
surface.setInvalidFrameRate(Float.NaN, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT,
changeStrategy);
assertEquals(initialNumEvents, mModeChangedEvents.size());
surface.setInvalidFrameRate(0.f, -10, changeStrategy);
assertEquals(initialNumEvents, mModeChangedEvents.size());
surface.setInvalidFrameRate(0.f, 50, changeStrategy);
assertEquals(initialNumEvents, mModeChangedEvents.size());
} finally {
if (surface != null) {
surface.release();
}
}
}
public void testInvalidParams() throws InterruptedException {
runTestsWithPreconditions(this::testInvalidParams, "invalid params behavior");
}
private void runOneSurfaceTest(Api api, OneSurfaceTestInterface test)
throws InterruptedException {
TestSurface surface = null;
try {
surface = new TestSurface(api, mSurfaceView.getSurfaceControl(), mSurface,
"testSurface", mSurfaceView.getHolder().getSurfaceFrame(),
/*visible=*/true, Color.RED);
test.run(surface);
} finally {
if (surface != null) {
surface.release();
}
}
}
private void testMatchContentFramerate_None(Api api) throws InterruptedException {
runOneSurfaceTest(api, (TestSurface surface) -> {
Display display = getDisplay();
Display.Mode currentMode = display.getMode();
List<Float> frameRates = getRefreshRates(currentMode, display);
for (float frameRate : frameRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
Surface.CHANGE_FRAME_RATE_ALWAYS);
assertEquals("Mode switches are not expected but these were detected "
+ modeSwitchesToString(initialNumEvents, mModeChangedEvents.size()),
mModeChangedEvents.size(), initialNumEvents);
}
});
}
public void testMatchContentFramerate_None() throws InterruptedException {
runTestsWithPreconditions(this::testMatchContentFramerate_None,
"testMatchContentFramerate_None");
}
private void testMatchContentFramerate_Auto(Api api)
throws InterruptedException {
runOneSurfaceTest(api, (TestSurface surface) -> {
Display display = getDisplay();
Display.Mode currentMode = display.getMode();
List<Float> frameRatesToTest = Floats.asList(currentMode.getAlternativeRefreshRates());
for (float frameRate : frameRatesToTest) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
Surface.CHANGE_FRAME_RATE_ALWAYS);
verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_STRICT, surface);
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
}
// Reset to default
surface.setFrameRate(0.f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
Surface.CHANGE_FRAME_RATE_ALWAYS);
// Wait for potential mode switches.
waitForStableFrameRate(surface);
currentMode = display.getMode();
List<Float> seamedRefreshRates = getSeamedRefreshRates(currentMode, display);
for (float frameRate : seamedRefreshRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
Surface.CHANGE_FRAME_RATE_ALWAYS);
// Mode switches may have occurred, make sure they were all seamless.
verifyModeSwitchesAreSeamless(initialNumEvents, mModeChangedEvents.size());
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
}
});
}
public void testMatchContentFramerate_Auto() throws InterruptedException {
runTestsWithPreconditions(this::testMatchContentFramerate_Auto,
"testMatchContentFramerate_Auto");
}
private void testMatchContentFramerate_Always(Api api) throws InterruptedException {
runOneSurfaceTest(api, (TestSurface surface) -> {
Display display = getDisplay();
List<Float> frameRates = getRefreshRates(display.getMode(), display);
for (float frameRate : frameRates) {
int initialNumEvents = mModeChangedEvents.size();
surface.setFrameRate(frameRate, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
Surface.CHANGE_FRAME_RATE_ALWAYS);
verifyCompatibleAndStableFrameRate(frameRate, FRAME_RATE_TOLERANCE_STRICT, surface);
verifyModeSwitchesDontChangeResolution(initialNumEvents,
mModeChangedEvents.size());
}
});
}
public void testMatchContentFramerate_Always() throws InterruptedException {
runTestsWithPreconditions(this::testMatchContentFramerate_Always,
"testMatchContentFramerate_Always");
}
private static native int nativeWindowSetFrameRate(
Surface surface, float frameRate, int compatibility, int changeFrameRateStrategy);
private static native long nativeSurfaceControlCreate(
Surface parentSurface, String name, int left, int top, int right, int bottom);
private static native void nativeSurfaceControlDestroy(long surfaceControl);
private static native void nativeSurfaceControlSetFrameRate(
long surfaceControl, float frameRate, int compatibility, int changeFrameRateStrategy);
private static native void nativeSurfaceControlSetVisibility(
long surfaceControl, boolean visible);
private static native boolean nativeSurfaceControlPostBuffer(long surfaceControl, int color);
}