Merge changes from topic "psc1"

* changes:
  Attached choreographer tests
  Attach the Choreographer instance with SurfaceControl
This commit is contained in:
Rachel Lee
2023-02-02 18:45:48 +00:00
committed by Android (Google) Code Review
18 changed files with 793 additions and 13 deletions

View File

@@ -3148,6 +3148,9 @@ package android.view {
public final class SurfaceControl implements android.os.Parcelable {
ctor public SurfaceControl(@NonNull android.view.SurfaceControl, @NonNull String);
method @NonNull public android.view.Choreographer getChoreographer();
method @NonNull public android.view.Choreographer getChoreographer(@NonNull android.os.Looper);
method public boolean hasChoreographer();
method public boolean isSameSurface(@NonNull android.view.SurfaceControl);
}

View File

@@ -270,10 +270,14 @@ public final class Choreographer {
private static final int CALLBACK_LAST = CALLBACK_COMMIT;
private Choreographer(Looper looper, int vsyncSource) {
this(looper, vsyncSource, /* layerHandle */ 0L);
}
private Choreographer(Looper looper, int vsyncSource, long layerHandle) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
? new FrameDisplayEventReceiver(looper, vsyncSource, layerHandle)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
@@ -312,6 +316,26 @@ public final class Choreographer {
return sSfThreadInstance.get();
}
/**
* Gets the choreographer associated with the SurfaceControl.
*
* @param layerHandle to which the choreographer will be attached.
* @param looper the choreographer is attached on this looper.
*
* @return The choreographer for the looper which is attached
* to the sourced SurfaceControl::mNativeHandle.
* @throws IllegalStateException if the looper sourced is null.
* @hide
*/
@NonNull
static Choreographer getInstanceForSurfaceControl(long layerHandle,
@NonNull Looper looper) {
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper, VSYNC_SOURCE_APP, layerHandle);
}
/**
* @return The Choreographer of the main thread, if it exists, or {@code null} otherwise.
* @hide
@@ -333,6 +357,15 @@ public final class Choreographer {
mDisplayEventReceiver.dispose();
}
/**
* Dispose the DisplayEventReceiver on the Choreographer.
* @hide
*/
@UnsupportedAppUsage
void invalidate() {
dispose();
}
/**
* The amount of time, in milliseconds, between each frame of the animation.
* <p>
@@ -1166,8 +1199,8 @@ public final class Choreographer {
private int mFrame;
private VsyncEventData mLastVsyncEventData = new VsyncEventData();
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, 0);
FrameDisplayEventReceiver(Looper looper, int vsyncSource, long layerHandle) {
super(looper, vsyncSource, /* eventRegistration */ 0, layerHandle);
}
// TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for

View File

@@ -80,7 +80,7 @@ public abstract class DisplayEventReceiver {
private MessageQueue mMessageQueue;
private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
MessageQueue messageQueue, int vsyncSource, int eventRegistration);
MessageQueue messageQueue, int vsyncSource, int eventRegistration, long layerHandle);
private static native void nativeDispose(long receiverPtr);
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);
@@ -93,7 +93,11 @@ public abstract class DisplayEventReceiver {
*/
@UnsupportedAppUsage
public DisplayEventReceiver(Looper looper) {
this(looper, VSYNC_SOURCE_APP, 0);
this(looper, VSYNC_SOURCE_APP, /* eventRegistration */ 0, /* layerHandle */ 0L);
}
public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
this(looper, vsyncSource, eventRegistration, /* layerHandle */ 0L);
}
/**
@@ -103,15 +107,17 @@ public abstract class DisplayEventReceiver {
* @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
* @param eventRegistration Which events to dispatch. Must be a bitfield consist of the
* EVENT_REGISTRATION_*_FLAG values.
* @param layerHandle Layer to which the current instance is attached to
*/
public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration,
long layerHandle) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource, eventRegistration);
vsyncSource, eventRegistration, layerHandle);
}
@Override

View File

@@ -64,6 +64,7 @@ import android.opengl.EGLDisplay;
import android.opengl.EGLSync;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
@@ -463,6 +464,10 @@ public final class SurfaceControl implements Parcelable {
public long mNativeObject;
private long mNativeHandle;
private final Object mChoreographerLock = new Object();
@GuardedBy("mChoreographerLock")
private Choreographer mChoreographer;
// TODO: Move width/height to native and fix locking through out.
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -1268,6 +1273,59 @@ public final class SurfaceControl implements Parcelable {
return other.mNativeHandle == mNativeHandle;
}
/**
* Returns the associated {@link Choreographer} instance with the
* current instance of the SurfaceControl.
* Must be called from a thread that already has a {@link android.os.Looper}
* associated with it.
* If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
* of the {@link Choreographer} is created.
*
* @hide
*/
@TestApi
public @NonNull Choreographer getChoreographer() {
return getChoreographer(Looper.myLooper());
}
/**
* Returns the associated {@link Choreographer} instance with the
* current instance of the SurfaceControl.
* If there is no {@link Choreographer} associated with the SurfaceControl then a new instance
* of the {@link Choreographer} is created.
*
* @param looper the choreographer is attached on this looper
*
* @hide
*/
@TestApi
public @NonNull Choreographer getChoreographer(@NonNull Looper looper) {
checkNotReleased();
synchronized (mChoreographerLock) {
if (mChoreographer != null) {
return mChoreographer;
}
mChoreographer = Choreographer.getInstanceForSurfaceControl(mNativeHandle, looper);
return mChoreographer;
}
}
/**
* Returns true if {@link Choreographer} is present otherwise false.
* To check the validity use {@link #isValid} on the SurfaceControl, a valid SurfaceControl with
* choreographer will have the valid Choreographer.
*
* @hide
*/
@TestApi
@UnsupportedAppUsage
public boolean hasChoreographer() {
synchronized (mChoreographerLock) {
return mChoreographer != null;
}
}
/**
* Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
* android.view.SurfaceControlProto}.
@@ -1325,6 +1383,13 @@ public final class SurfaceControl implements Parcelable {
mNativeObject = 0;
mNativeHandle = 0;
mCloseGuard.close();
synchronized (mChoreographerLock) {
if (mChoreographer != null) {
mChoreographer.invalidate();
// TODO(b/266121235): Use NativeAllocationRegistry to clean up Choreographer.
mChoreographer = null;
}
}
}
}

View File

@@ -65,7 +65,7 @@ class NativeDisplayEventReceiver : public DisplayEventDispatcher {
public:
NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
const sp<MessageQueue>& messageQueue, jint vsyncSource,
jint eventRegistration);
jint eventRegistration, jlong layerHandle);
void dispose();
@@ -88,11 +88,15 @@ private:
NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
const sp<MessageQueue>& messageQueue,
jint vsyncSource, jint eventRegistration)
jint vsyncSource, jint eventRegistration,
jlong layerHandle)
: DisplayEventDispatcher(messageQueue->getLooper(),
static_cast<gui::ISurfaceComposer::VsyncSource>(vsyncSource),
static_cast<gui::ISurfaceComposer::EventRegistration>(
eventRegistration)),
eventRegistration),
layerHandle != 0 ? sp<IBinder>::fromExisting(
reinterpret_cast<IBinder*>(layerHandle))
: nullptr),
mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
mMessageQueue(messageQueue) {
ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -214,7 +218,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
}
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj,
jint vsyncSource, jint eventRegistration) {
jint vsyncSource, jint eventRegistration, jlong layerHandle) {
sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
if (messageQueue == NULL) {
jniThrowRuntimeException(env, "MessageQueue is not initialized.");
@@ -223,7 +227,7 @@ static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject
sp<NativeDisplayEventReceiver> receiver =
new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource,
eventRegistration);
eventRegistration, layerHandle);
status_t status = receiver->initialize();
if (status) {
String8 message;
@@ -268,7 +272,7 @@ static jobject nativeGetLatestVsyncEventData(JNIEnv* env, jclass clazz, jlong re
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;II)J",
{"nativeInit", "(Ljava/lang/ref/WeakReference;Landroid/os/MessageQueue;IIJ)J",
(void*)nativeInit},
{"nativeDispose", "(J)V", (void*)nativeDispose},
// @FastNative

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_test {
name: "ChoreographerTests",
srcs: ["src/**/*.java"],
libs: [
"android.test.runner",
"android.test.base",
],
static_libs: [
"androidx.test.core",
"androidx.test.ext.junit",
"androidx.test.rules",
"compatibility-device-util-axt",
"com.google.android.material_material",
"truth-prebuilt",
],
resource_dirs: ["src/main/res"],
certificate: "platform",
platform_apis: true,
test_suites: ["device-tests"],
optimize: {
enabled: false,
},
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.view.choreographertests">
<application android:debuggable="true" android:testOnly="true">
<uses-library android:name="android.test.runner"/>
<activity
android:name=".GraphicsActivity"
android:exported="false">
</activity>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="android.view.choreographertests"
android:label="Tests of android.view.ChoreographerTests">
</instrumentation>
</manifest>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 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.
-->
<configuration description="Config for ChoreographerTests cases">
<option name="test-suite-tag" value="cts" />
<option name="test-tag" value="ChoreographerTests"/>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="install-arg" value="-t" />
<option name="test-file-name" value="ChoreographerTests.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.view.choreographertests" />
<option name="hidden-api-checks" value="false" />
<option name="isolated-storage" value="false" />
</test>
</configuration>

View File

@@ -0,0 +1,2 @@
include platform/frameworks/base:/graphics/java/android/graphics/OWNERS
include platform/frameworks/native:/services/surfaceflinger/OWNERS

View File

@@ -0,0 +1,7 @@
{
"presubmit": [
{
"name": "ChoreographerTests"
}
]
}

View File

@@ -0,0 +1,474 @@
/*
* Copyright 2023 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.view.choreographertests;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.Manifest;
import android.app.compat.CompatChanges;
import android.hardware.display.DisplayManager;
import android.os.Looper;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class AttachedChoreographerTest {
private static final String TAG = "AttachedChoreographerTest";
private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758;
private static final int THRESHOLD_MS = 10;
private static final int CALLBACK_TIME_10_FPS = 100;
private static final int CALLBACK_TIME_30_FPS = 33;
private static final int FRAME_ITERATIONS = 21;
private static final int CALLBACK_MISSED_THRESHOLD = 2;
private final CountDownLatch mTestCompleteSignal = new CountDownLatch(2);
private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
private final CountDownLatch mNoCallbackSignal = new CountDownLatch(1);
private final CountDownLatch mFramesSignal = new CountDownLatch(FRAME_ITERATIONS);
private ActivityScenario<GraphicsActivity> mScenario;
private int mInitialMatchContentFrameRate;
private DisplayManager mDisplayManager;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private boolean mIsFirstCallback = true;
private int mCallbackMissedCounter = 0;
@Before
public void setUp() throws Exception {
mScenario = ActivityScenario.launch(GraphicsActivity.class);
mScenario.moveToState(Lifecycle.State.CREATED);
mCallbackMissedCounter = 0;
mScenario.onActivity(activity -> {
mSurfaceView = activity.findViewById(R.id.surface);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceCreationCountDown.countDown();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
});
UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
uiDevice.wakeUp();
uiDevice.executeShellCommand("wm dismiss-keyguard");
mScenario.moveToState(Lifecycle.State.RESUMED);
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
Manifest.permission.MANAGE_GAME_MODE);
mScenario.onActivity(activity -> {
mDisplayManager = activity.getSystemService(DisplayManager.class);
mInitialMatchContentFrameRate = toSwitchingType(
mDisplayManager.getMatchContentFrameRateUserPreference());
mDisplayManager.setRefreshRateSwitchingType(
DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
boolean changeIsEnabled =
CompatChanges.isChangeEnabled(
DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID);
Log.i(TAG, "DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGE_ID is "
+ (changeIsEnabled ? "enabled" : "disabled"));
});
}
@After
public void tearDown() {
mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
@Test
public void test_create_choreographer() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
mTestCompleteSignal.countDown();
SurfaceControl sc1 = new SurfaceControl(sc, "AttachedChoreographerTests");
// Create attached choreographer with getChoreographer
sc1.getChoreographer();
assertTrue(sc1.hasChoreographer());
assertTrue(sc1.isValid());
sc1.release();
SurfaceControl sc2 = new SurfaceControl(sc, "AttachedChoreographerTests");
// Create attached choreographer with Looper.myLooper
sc2.getChoreographer(Looper.myLooper());
assertTrue(sc2.hasChoreographer());
assertTrue(sc2.isValid());
sc2.release();
SurfaceControl sc3 = new SurfaceControl(sc, "AttachedChoreographerTests");
// Create attached choreographer with Looper.myLooper
sc3.getChoreographer(Looper.getMainLooper());
assertTrue(sc3.hasChoreographer());
assertTrue(sc3.isValid());
sc3.release();
mTestCompleteSignal.countDown();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_copy_surface_control() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
// Create attached choreographer
sc.getChoreographer();
assertTrue(sc.hasChoreographer());
// Use copy constructor
SurfaceControl copyConstructorSc = new SurfaceControl(sc, "AttachedChoreographerTests");
//Choreographer isn't copied over.
assertFalse(copyConstructorSc.hasChoreographer());
copyConstructorSc.getChoreographer();
assertTrue(copyConstructorSc.hasChoreographer());
mTestCompleteSignal.countDown();
// Use copyFrom
SurfaceControl copyFromSc = new SurfaceControl();
copyFromSc.copyFrom(sc, "AttachedChoreographerTests");
//Choreographer isn't copied over.
assertFalse(copyFromSc.hasChoreographer());
copyFromSc.getChoreographer();
assertTrue(copyFromSc.hasChoreographer());
mTestCompleteSignal.countDown();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_mirror_surface_control() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
// Create attached choreographer
sc.getChoreographer();
assertTrue(sc.hasChoreographer());
mTestCompleteSignal.countDown();
// Use mirrorSurface
SurfaceControl mirrorSc = SurfaceControl.mirrorSurface(sc);
//Choreographer isn't copied over.
assertFalse(mirrorSc.hasChoreographer());
mirrorSc.getChoreographer();
assertTrue(mirrorSc.hasChoreographer());
// make SurfaceControl invalid by releasing it.
mirrorSc.release();
assertTrue(sc.isValid());
assertFalse(mirrorSc.isValid());
assertFalse(mirrorSc.hasChoreographer());
assertThrows(NullPointerException.class, mirrorSc::getChoreographer);
mTestCompleteSignal.countDown();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_postFrameCallback() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
sc.getChoreographer().postFrameCallback(
frameTimeNanos -> mTestCompleteSignal.countDown());
SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
Choreographer copyChoreographer = copySc.getChoreographer();
// make SurfaceControl invalid by releasing it.
copySc.release();
assertTrue(sc.isValid());
assertFalse(copySc.isValid());
copyChoreographer.postFrameCallback(frameTimeNanos -> mNoCallbackSignal.countDown());
assertDoesReceiveCallback();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_postFrameCallbackDelayed() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
sc.getChoreographer(Looper.getMainLooper()).postFrameCallbackDelayed(
callback -> mTestCompleteSignal.countDown(),
/* delayMillis */ 5);
SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
Choreographer copyChoreographer = copySc.getChoreographer();
// make SurfaceControl invalid by releasing it.
copySc.release();
assertTrue(sc.isValid());
assertFalse(copySc.isValid());
copyChoreographer.postFrameCallbackDelayed(
frameTimeNanos -> mNoCallbackSignal.countDown(), /* delayMillis */5);
assertDoesReceiveCallback();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_postCallback() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
sc.getChoreographer().postCallback(Choreographer.CALLBACK_COMMIT,
mTestCompleteSignal::countDown, /* token */ this);
SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
Choreographer copyChoreographer = copySc.getChoreographer();
// make SurfaceControl invalid by releasing it.
copySc.release();
assertTrue(sc.isValid());
assertFalse(copySc.isValid());
copyChoreographer.postCallback(Choreographer.CALLBACK_COMMIT,
mNoCallbackSignal::countDown, /* token */ this);
assertDoesReceiveCallback();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_postCallbackDelayed() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
sc.getChoreographer().postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
mTestCompleteSignal::countDown, /* token */ this, /* delayMillis */ 5);
SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
Choreographer copyChoreographer = copySc.getChoreographer();
// make SurfaceControl invalid by releasing it.
copySc.release();
assertTrue(sc.isValid());
assertFalse(copySc.isValid());
copyChoreographer.postCallbackDelayed(Choreographer.CALLBACK_COMMIT,
mNoCallbackSignal::countDown, /* token */ this, /* delayMillis */ 5);
assertDoesReceiveCallback();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_postVsyncCallback() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeout */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
sc.getChoreographer().postVsyncCallback(data -> mTestCompleteSignal.countDown());
SurfaceControl copySc = new SurfaceControl(sc, "AttachedChoreographerTests");
Choreographer copyChoreographer = copySc.getChoreographer();
// make SurfaceControl invalid by releasing it.
copySc.release();
assertTrue(sc.isValid());
assertFalse(copySc.isValid());
copyChoreographer.postVsyncCallback(data -> mNoCallbackSignal.countDown());
assertDoesReceiveCallback();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 2L)) {
fail("Test not finished in 2 Seconds");
}
}
@Test
public void test_choreographer_10Hz_refreshRate() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
Choreographer choreographer = sc.getChoreographer();
SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
transaction.setFrameRate(sc, 10.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
.addTransactionCommittedListener(Runnable::run,
() -> verifyVsyncCallbacks(choreographer,
CALLBACK_TIME_10_FPS))
.apply();
mTestCompleteSignal.countDown();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
fail("Test not finished in 5 Seconds");
}
}
@Test
public void test_choreographer_30Hz_refreshRate() {
mScenario.onActivity(activity -> {
if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) {
fail("Unable to create surface within 1 Second");
}
SurfaceControl sc = mSurfaceView.getSurfaceControl();
Choreographer choreographer = sc.getChoreographer();
SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
transaction.setFrameRate(sc, 30.0f, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE)
.addTransactionCommittedListener(Runnable::run,
() -> verifyVsyncCallbacks(choreographer,
CALLBACK_TIME_30_FPS))
.apply();
mTestCompleteSignal.countDown();
});
if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds */ 5L)) {
fail("Test not finished in 5 Seconds");
}
}
private void verifyVsyncCallbacks(Choreographer choreographer, int callbackDurationMs) {
long callbackRequestedTimeNs = System.nanoTime();
choreographer.postVsyncCallback(frameData -> {
mFramesSignal.countDown();
final long frameCount = mFramesSignal.getCount();
if (frameCount > 0) {
if (!mIsFirstCallback) {
// Skip the first callback as it takes 1 frame
// to reflect the new refresh rate
long callbackDurationDiffMs = getCallbackDurationDiffInMs(
frameData.getFrameTimeNanos(),
callbackRequestedTimeNs, callbackDurationMs);
if (callbackDurationDiffMs < 0 || callbackDurationDiffMs > THRESHOLD_MS) {
mCallbackMissedCounter++;
Log.e(TAG, "Frame #" + Math.abs(frameCount - FRAME_ITERATIONS)
+ " vsync callback failed, expected callback in "
+ callbackDurationMs
+ " With threshold of " + THRESHOLD_MS
+ " but actual duration difference is " + callbackDurationDiffMs);
}
}
mIsFirstCallback = false;
verifyVsyncCallbacks(choreographer, callbackDurationMs);
} else {
assertTrue("Missed timeline for " + mCallbackMissedCounter + " callbacks, while "
+ CALLBACK_MISSED_THRESHOLD + " missed callbacks are allowed",
mCallbackMissedCounter <= CALLBACK_MISSED_THRESHOLD);
mTestCompleteSignal.countDown();
}
});
}
private long getCallbackDurationDiffInMs(long callbackTimeNs, long requestedTimeNs,
int expectedCallbackMs) {
long actualTimeMs = TimeUnit.NANOSECONDS.toMillis(callbackTimeNs)
- TimeUnit.NANOSECONDS.toMillis(requestedTimeNs);
return Math.abs(expectedCallbackMs - actualTimeMs);
}
private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) {
try {
return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
throw new AssertionError("Test interrupted", ex);
}
}
private int toSwitchingType(int matchContentFrameRateUserPreference) {
switch (matchContentFrameRateUserPreference) {
case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
return DisplayManager.SWITCHING_TYPE_NONE;
case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
default:
return -1;
}
}
private void assertDoesReceiveCallback() {
try {
if (mNoCallbackSignal.await(/* timeout */ 50L, TimeUnit.MILLISECONDS)) {
fail("Callback not supposed to be generated");
} else {
mTestCompleteSignal.countDown();
}
} catch (InterruptedException e) {
fail("Callback wait is interrupted " + e);
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2023 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.view.choreographertests;
import android.app.Activity;
import android.os.Bundle;
public class GraphicsActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_attached_choreographer);
}
}

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 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.
-->
<resources>
<string name="app_name">ChoreographerTests</string>
</resources>