summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--native/android/libandroid.map.txt1
-rw-r--r--native/android/surface_control.cpp12
-rw-r--r--tests/ChoreographerTests/Android.bp3
-rw-r--r--tests/ChoreographerTests/jni/Android.bp44
-rw-r--r--tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp33
-rw-r--r--tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp167
-rw-r--r--tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java179
7 files changed, 439 insertions, 0 deletions
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 987b23fdd1fd..f258c27aa070 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -345,6 +345,7 @@ LIBANDROID_PLATFORM {
extern "C++" {
ASurfaceControl_registerSurfaceStatsListener*;
ASurfaceControl_unregisterSurfaceStatsListener*;
+ ASurfaceControl_getChoreographer*;
ASurfaceControlStats_getAcquireTime*;
ASurfaceControlStats_getFrameNumber*;
};
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index ea20c6c9e0b1..b7f359602a5d 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -180,6 +180,18 @@ void ASurfaceControl_unregisterSurfaceStatsListener(void* context,
reinterpret_cast<void*>(func));
}
+AChoreographer* ASurfaceControl_getChoreographer(ASurfaceControl* aSurfaceControl) {
+ LOG_ALWAYS_FATAL_IF(aSurfaceControl == nullptr, "aSurfaceControl should not be nullptr");
+ SurfaceControl* surfaceControl =
+ ASurfaceControl_to_SurfaceControl(reinterpret_cast<ASurfaceControl*>(aSurfaceControl));
+ if (!surfaceControl->isValid()) {
+ ALOGE("Attempted to get choreographer from invalid surface control");
+ return nullptr;
+ }
+ SurfaceControl_acquire(surfaceControl);
+ return reinterpret_cast<AChoreographer*>(surfaceControl->getChoreographer().get());
+}
+
int64_t ASurfaceControlStats_getAcquireTime(ASurfaceControlStats* stats) {
if (const auto* fence = std::get_if<sp<Fence>>(&stats->acquireTimeOrFence)) {
// We got a fence instead of the acquire time due to latch unsignaled.
diff --git a/tests/ChoreographerTests/Android.bp b/tests/ChoreographerTests/Android.bp
index ff252f717591..ca3026705c63 100644
--- a/tests/ChoreographerTests/Android.bp
+++ b/tests/ChoreographerTests/Android.bp
@@ -36,6 +36,9 @@ android_test {
"com.google.android.material_material",
"truth-prebuilt",
],
+ jni_libs: [
+ "libchoreographertests_jni",
+ ],
resource_dirs: ["src/main/res"],
certificate: "platform",
platform_apis: true,
diff --git a/tests/ChoreographerTests/jni/Android.bp b/tests/ChoreographerTests/jni/Android.bp
new file mode 100644
index 000000000000..7198c511489b
--- /dev/null
+++ b/tests/ChoreographerTests/jni/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test_library {
+ name: "libchoreographertests_jni",
+ cflags: [
+ "-Werror",
+ "-Wthread-safety",
+ ],
+
+ gtest: false,
+
+ srcs: [
+ "ChoreographerTestsJniOnLoad.cpp",
+ "android_view_tests_ChoreographerNativeTest.cpp",
+ ],
+
+ shared_libs: [
+ "libandroid",
+ "libnativehelper",
+ "liblog",
+ ],
+
+ header_libs: [
+ "libandroid_headers_private",
+ ],
+
+ stl: "c++_static",
+}
diff --git a/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp b/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
new file mode 100644
index 000000000000..447376fca78f
--- /dev/null
+++ b/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+#include <jni.h>
+
+#define LOG_TAG "ChoreographerTestsJniOnLoad"
+
+extern int register_android_android_view_tests_ChoreographerNativeTest(JNIEnv* env);
+
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv* env = NULL;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ return JNI_ERR;
+ }
+
+ if (register_android_android_view_tests_ChoreographerNativeTest(env)) {
+ return JNI_ERR;
+ }
+
+ return JNI_VERSION_1_6;
+} \ No newline at end of file
diff --git a/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp b/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp
new file mode 100644
index 000000000000..27f4bae9e65a
--- /dev/null
+++ b/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ *
+ */
+
+#include <android/choreographer.h>
+#include <android/log.h>
+#include <android/surface_control_jni.h>
+#include <jni.h>
+#include <private/surface_control_private.h>
+#include <time.h>
+#include <utils/Log.h>
+#include <utils/Mutex.h>
+
+#include <chrono>
+#include <cmath>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+#undef LOG_TAG
+#define LOG_TAG "AttachedChoreographerNativeTest"
+
+// Copied from cts/tests/tests/view/jni/jniAssert.h, to be removed when integrated in CTS.
+#define ASSERT(condition, format, args...) \
+ if (!(condition)) { \
+ fail(env, format, ##args); \
+ return; \
+ }
+
+using namespace std::chrono_literals;
+
+static constexpr std::chrono::nanoseconds kMaxRuntime{1s};
+static constexpr float kFpsTolerance = 5.0f;
+
+static constexpr int kNumOfFrames = 20;
+
+struct {
+ struct {
+ jclass clazz;
+ jmethodID endTest;
+ } attachedChoreographerNativeTest;
+} gJni;
+
+struct CallbackData {
+ std::mutex mutex;
+
+ // Condition to signal callbacks are done running and test can be verified.
+ std::condition_variable_any condition;
+
+ // Flag to ensure not to lock on the condition if notify is called before wait_for.
+ bool callbacksComplete = false;
+
+ AChoreographer* choreographer = nullptr;
+ int count GUARDED_BY(mutex){0};
+ std::chrono::nanoseconds frameTime GUARDED_BY(mutex){0};
+ std::chrono::nanoseconds startTime;
+ std::chrono::nanoseconds endTime GUARDED_BY(mutex){0};
+};
+
+static std::chrono::nanoseconds now() {
+ return std::chrono::steady_clock::now().time_since_epoch();
+}
+
+static void vsyncCallback(const AChoreographerFrameCallbackData* callbackData, void* data) {
+ ALOGI("%s: Vsync callback running", __func__);
+ long frameTimeNanos = AChoreographerFrameCallbackData_getFrameTimeNanos(callbackData);
+
+ auto* cb = static_cast<CallbackData*>(data);
+ {
+ std::lock_guard<std::mutex> _l(cb->mutex);
+ cb->count++;
+ cb->endTime = now();
+ cb->frameTime = std::chrono::nanoseconds{frameTimeNanos};
+
+ ALOGI("%s: ran callback now %ld, frameTimeNanos %ld, new count %d", __func__,
+ static_cast<long>(cb->endTime.count()), frameTimeNanos, cb->count);
+ if (cb->endTime - cb->startTime > kMaxRuntime) {
+ cb->callbacksComplete = true;
+ cb->condition.notify_all();
+ return;
+ }
+ }
+
+ ALOGI("%s: Posting next callback", __func__);
+ AChoreographer_postVsyncCallback(cb->choreographer, vsyncCallback, data);
+}
+
+static void fail(JNIEnv* env, const char* format, ...) {
+ va_list args;
+
+ va_start(args, format);
+ char* msg;
+ int rc = vasprintf(&msg, format, args);
+ va_end(args);
+
+ jclass exClass;
+ const char* className = "java/lang/AssertionError";
+ exClass = env->FindClass(className);
+ env->ThrowNew(exClass, msg);
+ free(msg);
+}
+
+jlong SurfaceControl_getChoreographer(JNIEnv* env, jclass, jobject surfaceControlObj) {
+ return reinterpret_cast<jlong>(
+ ASurfaceControl_getChoreographer(ASurfaceControl_fromJava(env, surfaceControlObj)));
+}
+
+static bool frameRateEquals(float fr1, float fr2) {
+ return std::abs(fr1 - fr2) <= kFpsTolerance;
+}
+
+static void endTest(JNIEnv* env, jobject clazz) {
+ env->CallVoidMethod(clazz, gJni.attachedChoreographerNativeTest.endTest);
+}
+
+static void android_view_ChoreographerNativeTest_testPostVsyncCallbackAtFrameRate(
+ JNIEnv* env, jobject clazz, jlong choreographerPtr, jfloat expectedFrameRate) {
+ AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
+ CallbackData cb;
+ cb.choreographer = choreographer;
+ cb.startTime = now();
+ ALOGI("%s: Post first callback at %ld", __func__, static_cast<long>(cb.startTime.count()));
+ AChoreographer_postVsyncCallback(choreographer, vsyncCallback, &cb);
+
+ std::scoped_lock<std::mutex> conditionLock(cb.mutex);
+ ASSERT(cb.condition.wait_for(cb.mutex, 2 * kMaxRuntime, [&cb] { return cb.callbacksComplete; }),
+ "Never received callbacks!");
+
+ float actualFrameRate = static_cast<float>(cb.count) /
+ (static_cast<double>((cb.endTime - cb.startTime).count()) / 1'000'000'000.0);
+ ALOGI("%s: callback called %d times with final start time %ld, end time %ld, effective "
+ "frame rate %f",
+ __func__, cb.count, static_cast<long>(cb.startTime.count()),
+ static_cast<long>(cb.endTime.count()), actualFrameRate);
+ ASSERT(frameRateEquals(actualFrameRate, expectedFrameRate),
+ "Effective frame rate is %f but expected to be %f", actualFrameRate, expectedFrameRate);
+
+ endTest(env, clazz);
+}
+
+static JNINativeMethod gMethods[] = {
+ {"nativeSurfaceControl_getChoreographer", "(Landroid/view/SurfaceControl;)J",
+ (void*)SurfaceControl_getChoreographer},
+ {"nativeTestPostVsyncCallbackAtFrameRate", "(JF)V",
+ (void*)android_view_ChoreographerNativeTest_testPostVsyncCallbackAtFrameRate},
+};
+
+int register_android_android_view_tests_ChoreographerNativeTest(JNIEnv* env) {
+ jclass clazz =
+ env->FindClass("android/view/choreographertests/AttachedChoreographerNativeTest");
+ gJni.attachedChoreographerNativeTest.clazz = static_cast<jclass>(env->NewGlobalRef(clazz));
+ gJni.attachedChoreographerNativeTest.endTest = env->GetMethodID(clazz, "endTest", "()V");
+ return env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));
+}
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java
new file mode 100644
index 000000000000..1118eb3bfc6e
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 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.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.Manifest;
+import android.hardware.display.DisplayManager;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+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 AttachedChoreographerNativeTest {
+ private static final String TAG = "AttachedChoreographerNativeTest";
+
+ static {
+ System.loadLibrary("choreographertests_jni");
+ }
+
+ private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
+ private CountDownLatch mTestCompleteSignal;
+ private long mChoreographerPtr;
+ private SurfaceView mSurfaceView;
+ private SurfaceHolder mSurfaceHolder;
+ private ActivityScenario<GraphicsActivity> mScenario;
+ private int mInitialMatchContentFrameRate;
+ private DisplayManager mDisplayManager;
+
+ private static native long nativeSurfaceControl_getChoreographer(SurfaceControl surfaceControl);
+ private native void nativeTestPostVsyncCallbackAtFrameRate(
+ long choreographerPtr, float expectedFrameRate);
+
+ @Before
+ public void setup() throws Exception {
+ mScenario = ActivityScenario.launch(GraphicsActivity.class);
+ mScenario.moveToState(Lifecycle.State.CREATED);
+ 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) {}
+ });
+ });
+
+ mScenario.moveToState(Lifecycle.State.RESUMED);
+ UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ uiDevice.wakeUp();
+ uiDevice.executeShellCommand("wm dismiss-keyguard");
+
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ android.Manifest.permission.LOG_COMPAT_CHANGE,
+ android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
+ android.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);
+ });
+ }
+
+ @After
+ public void tearDown() {
+ mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
+ mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void test_choreographer_callbacksForVariousFrameRates() {
+ for (int divisor : new int[] {2, 3}) {
+ mTestCompleteSignal = new CountDownLatch(1);
+ mScenario.onActivity(activity -> {
+ if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds= */ 1L)) {
+ fail("Unable to create surface within 1 Second");
+ }
+
+ SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl();
+ mChoreographerPtr = nativeSurfaceControl_getChoreographer(surfaceControl);
+ Log.i(TAG, "mChoreographerPtr value " + mChoreographerPtr);
+
+ float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate();
+ float expectedFrameRate = displayRefreshRate / divisor;
+
+ SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+ transaction
+ .setFrameRate(surfaceControl, expectedFrameRate,
+ Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)
+ .addTransactionCommittedListener(Runnable::run,
+ () -> {
+ assertTrue(mChoreographerPtr != 0L);
+ Log.i(TAG, "Testing frame rate of " + expectedFrameRate);
+ nativeTestPostVsyncCallbackAtFrameRate(
+ mChoreographerPtr, expectedFrameRate);
+ })
+ .apply();
+ });
+ // wait for the previous callbacks to finish before moving to the next divisor
+ if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds= */ 5L)) {
+ fail("Test for divisor " + divisor + " not finished in 5 seconds");
+ }
+ }
+ }
+
+ /** Call from native to trigger test completion. */
+ private void endTest() {
+ Log.i(TAG, "Signal test completion!");
+ mTestCompleteSignal.countDown();
+ }
+
+ 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;
+ }
+ }
+}