Merge changes from topic "psc-ndk"

* changes:
  NDK attached choreographer tests
  NDK attached Choreographer from SurfaceControl.
This commit is contained in:
TreeHugger Robot
2023-02-10 02:17:44 +00:00
committed by Android (Google) Code Review
7 changed files with 439 additions and 0 deletions

View File

@@ -345,6 +345,7 @@ LIBANDROID_PLATFORM {
extern "C++" {
ASurfaceControl_registerSurfaceStatsListener*;
ASurfaceControl_unregisterSurfaceStatsListener*;
ASurfaceControl_getChoreographer*;
ASurfaceControlStats_getAcquireTime*;
ASurfaceControlStats_getFrameNumber*;
};

View File

@@ -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.

View File

@@ -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,

View File

@@ -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",
}

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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;
}
}
}