diff options
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; + } + } +} |