Simple test of buffer presentation time stamps.
Includes a bit of extra code that should either be turned into a
real test or removed. (Currently suppressed from execution.)
Bug 7900302
Change-Id: Idf1c01e382cb4ff9272c48528d02ca6463df00a2
diff --git a/tests/tests/media/src/android/media/cts/InputSurface.java b/tests/tests/media/src/android/media/cts/InputSurface.java
index 5397295..5d72979 100644
--- a/tests/tests/media/src/android/media/cts/InputSurface.java
+++ b/tests/tests/media/src/android/media/cts/InputSurface.java
@@ -142,6 +142,13 @@
}
}
+ public void makeUnCurrent() {
+ if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
+ EGL14.EGL_NO_CONTEXT)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+ }
+ }
+
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*/
@@ -157,6 +164,24 @@
}
/**
+ * Queries the surface's width.
+ */
+ public int getWidth() {
+ int[] value = new int[1];
+ EGL14.eglQuerySurface(mEGLDisplay, mEGLSurface, EGL14.EGL_WIDTH, value, 0);
+ return value[0];
+ }
+
+ /**
+ * Queries the surface's height.
+ */
+ public int getHeight() {
+ int[] value = new int[1];
+ EGL14.eglQuerySurface(mEGLDisplay, mEGLSurface, EGL14.EGL_HEIGHT, value, 0);
+ return value[0];
+ }
+
+ /**
* Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
*/
public void setPresentationTime(long nsecs) {
diff --git a/tests/tests/media/src/android/media/cts/PresentationSyncTest.java b/tests/tests/media/src/android/media/cts/PresentationSyncTest.java
new file mode 100644
index 0000000..2846602
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/PresentationSyncTest.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2013 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.media.cts;
+
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Trace;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+import android.view.Choreographer;
+import android.view.SurfaceHolder;
+
+
+/**
+ * Tests synchronized frame presentation.
+ *
+ * SurfaceFlinger allows a "desired presentation time" value to be passed along with buffers of
+ * data. This exercises that feature.
+ */
+public class PresentationSyncTest extends ActivityInstrumentationTestCase2<MediaStubActivity>
+ implements SurfaceHolder.Callback {
+ private static final String TAG = "RenderSyncTest";
+ private static final boolean VERBOSE = false; // lots of logging
+ private static final int FRAME_COUNT = 128; // ~2 sec @ 60fps
+
+ // message values
+ private static final int START_TEST = 0;
+ private static final int END_TEST = 1;
+
+ // width and height of the Surface we're given to draw on
+ private int mWidth;
+ private int mHeight;
+
+ public PresentationSyncTest() {
+ super(MediaStubActivity.class);
+ }
+
+ /**
+ * Tests whether the output frame rate can be limited by the presentation time.
+ * <p>
+ * Generates and displays the same series of images three times. The first run uses "now"
+ * as the desired presentation time to establish an estimate of the refresh time. Later
+ * runs set the presentation time to (start_time + frame_number * refresh_time * multiplier),
+ * with the expectation that a multiplier of 2 will cause the animation to render at
+ * half speed.
+ * <p>
+ * This test does not use Choreographer. The longer the test runs, the farther out of
+ * phase the test will become with respect to the actual vsync timing.
+ * <p>
+ * Setting the presentation time for a frame is most easily done through an EGL extension,
+ * so we render each frame through GL.
+ *
+ * @throws Exception
+ */
+ //@Suppress
+ public void testThroughput() throws Exception {
+ // Get the Surface from the SurfaceView.
+ // TODO: is it safe to assume that it's ready?
+ SurfaceHolder holder = getActivity().getSurfaceHolder();
+ holder.addCallback(this);
+
+ // We use the width/height to render a simple series of patterns. If we get this
+ // wrong it shouldn't really matter -- some driver optimizations might make things
+ // faster, but it shouldn't affect how long it takes the frame to be displayed.
+ //
+ // We can get this from the View or from the EGLSurface. We don't have easy direct
+ // access to any of those things, so just ask our InputSurface to get it from EGL,
+ // since that's where we're drawing.
+ //
+ // Note: InputSurface was intended for a different purpose, but it's 99% right for our
+ // needs. Maybe rename it to "RecordableSurface"? Or trivially wrap it with a
+ // subclass that suppresses the EGL_RECORDABLE_ANDROID flag?
+ InputSurface output = new InputSurface(holder.getSurface());
+ mWidth = output.getWidth();
+ mHeight = output.getHeight();
+ Log.d(TAG, "Surface w=" + mWidth + " h=" + mHeight);
+ output.makeCurrent();
+
+ // Run a test with no presentation times specified. Assuming nothing else is
+ // fighting us for resources, all frames should display as quickly as possible,
+ // and we can estimate the refresh rate of the device.
+ long baseTimeNsec = runThroughputTest(output, 0L, -1);
+ long refreshNsec = baseTimeNsec / FRAME_COUNT;
+ Log.i(TAG, "Using " + refreshNsec + "ns as refresh rate");
+
+ // Run tests with times specified, at 1x, 1/2x, and 1/4x speed.
+ runThroughputTest(output, refreshNsec, 1);
+ runThroughputTest(output, refreshNsec, 2);
+ runThroughputTest(output, refreshNsec, 4);
+
+ output.release();
+ }
+
+ /**
+ * Runs the throughput test on the provided surface with the specified time values.
+ * <p>
+ * If mult is -1, the test runs in "training" mode, rendering frames as quickly as
+ * possible. This can be used to establish a baseline.
+ * <p>
+ * @return the test duration, in nanoseconds
+ */
+ private long runThroughputTest(InputSurface output, long frameTimeNsec, int mult) {
+ Log.d(TAG, "runThroughputTest: " + mult);
+ long startNsec = System.nanoTime();
+ long showNsec = 0;
+
+ if (true) {
+ // Output a frame that creates a "marker" in the --latency output
+ drawFrame(0, mult);
+ output.setPresentationTime(startNsec - 16700000L * 100);
+ Trace.beginSection("TEST BEGIN");
+ output.swapBuffers();
+ Trace.endSection();
+ startNsec = System.nanoTime();
+ }
+
+ for (int frameNum = 0; frameNum < FRAME_COUNT; frameNum++) {
+ if (mult != -1) {
+ showNsec = startNsec + frameNum * frameTimeNsec * mult;
+ //Log.d(TAG, "showNsec=" + showNsec);
+ }
+ drawFrame(frameNum, mult);
+ output.setPresentationTime(showNsec);
+ //Log.d(TAG, "ZZZ des=" + showNsec + " now=" + System.nanoTime());
+ Trace.beginSection("swapbuf " + frameNum);
+ output.swapBuffers();
+ Trace.endSection();
+ }
+
+ long endNsec = System.nanoTime();
+ long actualNsec = endNsec - startNsec;
+
+ if (mult != -1) {
+ // Some variation is inevitable, but we should be within a few percent of expected.
+ long expectedNsec = frameTimeNsec * FRAME_COUNT * mult;
+ long deltaNsec = Math.abs(expectedNsec - actualNsec);
+ double delta = (double) deltaNsec / expectedNsec;
+ if (delta > 0.1) {
+ Log.e(TAG, "mult=" + mult + ": expected=" + expectedNsec +
+ " actual=" + actualNsec + " p=" + delta);
+ // TODO: fail() instead of Log()
+ } else {
+ Log.d(TAG, "mult=" + mult + ": expected=" + expectedNsec +
+ " actual=" + actualNsec + " p=" + delta);
+ }
+ }
+ return endNsec - startNsec;
+ }
+
+
+ /**
+ * Exercises the test code, driving it off of Choreographer. The animation is driven at
+ * full speed, but with rendering requested at a future time. With each run the distance
+ * into the future is increased.
+ * <p>
+ * Loopers can't be reused once they quit, so it's easiest to create a new thread for
+ * each run.
+ * <p>
+ * (This isn't exactly a test -- it's primarily a way to exercise the code. Evaluate the
+ * results with "dumpsys SurfaceFlinger --latency SurfaceView" for each multiplier.
+ * The idea is to see frames where the desired-present is as close as possible to the
+ * actual-present, while still minimizing frame-ready. If we go too far into the future
+ * the BufferQueue will start to back up.)
+ * <p>
+ * @throws Exception
+ */
+ @Suppress
+ public void testChoreographed() throws Throwable {
+ // Get the Surface from the SurfaceView.
+ // TODO: is it safe to assume that it's ready?
+ SurfaceHolder holder = getActivity().getSurfaceHolder();
+ holder.addCallback(this);
+
+ InputSurface output = new InputSurface(holder.getSurface());
+ mWidth = output.getWidth();
+ mHeight = output.getHeight();
+ Log.d(TAG, "Surface w=" + mWidth + " h=" + mHeight);
+
+ for (int i = 1; i < 5; i++) {
+ ChoreographedWrapper.runTest(this, output, i);
+ }
+
+ output.release();
+ }
+
+ /**
+ * Shifts the test to a new thread, so we can manage our own Looper. Any exception
+ * thrown on the new thread is propagated to the caller.
+ */
+ private static class ChoreographedWrapper implements Runnable {
+ private final PresentationSyncTest mTest;
+ private final InputSurface mOutput;
+ private final int mFrameDelay;
+ private Throwable mThrowable;
+
+ private ChoreographedWrapper(PresentationSyncTest test, InputSurface output,
+ int frameDelay) {
+ mTest = test;
+ mOutput = output;
+ mFrameDelay = frameDelay;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mTest.runChoreographedTest(mOutput, mFrameDelay);
+ } catch (Throwable th) {
+ mThrowable = th;
+ }
+ }
+
+ /** Entry point. */
+ public static void runTest(PresentationSyncTest obj, InputSurface output,
+ int frameDelay) throws Throwable {
+ ChoreographedWrapper wrapper = new ChoreographedWrapper(obj, output, frameDelay);
+ Thread th = new Thread(wrapper, "sync test");
+ th.start();
+ th.join();
+ if (wrapper.mThrowable != null) {
+ throw wrapper.mThrowable;
+ }
+ }
+ }
+
+ /**
+ * Runs the test, driven by callbacks from the Looper we define here.
+ */
+ private void runChoreographedTest(InputSurface output, int frameDelay) {
+ Log.d(TAG, "runChoreographedTest");
+
+ output.makeCurrent();
+ final ChoRunner chore = new ChoRunner(output);
+
+ Looper.prepare();
+ Handler handler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case START_TEST:
+ Log.d(TAG, "Starting test");
+ chore.start(this, msg.arg1 /*frameDelay*/);
+ break;
+ case END_TEST:
+ Log.d(TAG, "Ending test");
+ Looper.myLooper().quitSafely();
+ break;
+ default:
+ Log.d(TAG, "unknown message " + msg.what);
+ break;
+ }
+ }
+ };
+
+ handler.sendMessage(Message.obtain(handler, START_TEST, frameDelay, 0));
+
+ Log.d(TAG, "looping (frameDelay=" + frameDelay + ")");
+ long startNanos = System.nanoTime();
+ Trace.beginSection("TEST BEGIN fd=" + frameDelay);
+ Looper.loop();
+ Trace.endSection();
+ long durationNanos = System.nanoTime() - startNanos;
+ Log.d(TAG, "loop exiting after " + durationNanos +
+ " (" + durationNanos / FRAME_COUNT + "ns)");
+
+ output.makeUnCurrent();
+ }
+
+
+ private class ChoRunner implements Choreographer.FrameCallback {
+ private final InputSurface mOutput;
+ private int mFrameDelay;
+ private Handler mHandler;
+ private int mCurFrame;
+ private Choreographer mChocho;
+ private long mPrevFrameTimeNanos;
+ private long mFrameDiff;
+
+ public ChoRunner(InputSurface output) {
+ mOutput = output;
+ }
+
+ public void start(Handler handler, int frameDelay) {
+ mHandler = handler;
+ mFrameDelay = frameDelay;
+
+ mCurFrame = 0;
+ mChocho = Choreographer.getInstance();
+ mChocho.postFrameCallback(this);
+ }
+
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (mPrevFrameTimeNanos != 0) {
+ // Update our vsync rate guess every frame so that, if we start with a
+ // stutter, we don't carry it for the whole test.
+ assertTrue(frameTimeNanos > mPrevFrameTimeNanos);
+ long prevDiff = frameTimeNanos - mPrevFrameTimeNanos;
+ if (mFrameDiff == 0 || mFrameDiff > prevDiff) {
+ mFrameDiff = prevDiff;
+ Log.d(TAG, "refresh rate approx " + mFrameDiff + "ns");
+ }
+
+ // If the current diff is >= 2x the expected frame time diff, we stuttered
+ // and need to drop a frame. (We might even need to drop more than one
+ // frame; ignoring that for now.)
+ if (prevDiff > mFrameDiff * 1.9) {
+ Log.d(TAG, "skip " + mCurFrame + " diff=" + prevDiff);
+ mCurFrame++;
+ }
+ }
+ mPrevFrameTimeNanos = frameTimeNanos;
+
+ if (mFrameDiff != 0) {
+ // set desired display time to N frames in the future, rather than ASAP.
+ //
+ // Note this is a "don't open until Xmas" feature. If vsyncs are happening
+ // at times T1, T2, T3, and we want the frame to appear onscreen when the
+ // buffers flip at T2, then we can theoretically request any time value
+ // in [T1, T2).
+ mOutput.setPresentationTime(frameTimeNanos + (mFrameDiff * mFrameDelay));
+ }
+
+ drawFrame(mCurFrame, mFrameDelay);
+ Trace.beginSection("swapbuf " + mCurFrame);
+ mOutput.swapBuffers();
+ Trace.endSection();
+
+ if (++mCurFrame < FRAME_COUNT) {
+ mChocho.postFrameCallback(this);
+ } else {
+ mHandler.sendMessage(Message.obtain(mHandler, END_TEST));
+ }
+ }
+ }
+
+ /**
+ * Draws a frame with GLES in the current context.
+ */
+ private void drawFrame(int num, int mult) {
+ num %= 64;
+ float colorVal;
+
+ if (mult > 0) {
+ colorVal = 1.0f / mult;
+ } else {
+ colorVal = 0.1f;
+ }
+
+ int startX, startY;
+ startX = (num % 16) * (mWidth / 16);
+ startY = (num / 16) * (mHeight / 4);
+ if ((num >= 16 && num < 32) || (num >= 48)) {
+ // reverse direction
+ startX = (mWidth - mWidth/16) - startX;
+ }
+
+ // clear screen
+ GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+
+ // draw rect
+ GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
+ GLES20.glScissor(startX, startY, mWidth / 16, mHeight / 4);
+ GLES20.glClearColor(colorVal, 1 - colorVal, 0.0f, 1.0f);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.d(TAG, "surfaceCreated");
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ // This doesn't seem to happen in practice with current test framework -- Surface is
+ // already created before we start, and the orientation is locked.
+ Log.d(TAG, "surfaceChanged f=" + format + " w=" + width + " h=" + height);
+ mWidth = width;
+ mHeight = height;
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.d(TAG, "surfaceDestroyed");
+ }
+}