blob: efa2e0a88a16ed9c09a5118e8f6a8f37e90279f4 [file] [log] [blame]
/*
* 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 = "PresentationSyncTest";
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
*/
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.0f);
long refreshNsec = baseTimeNsec / FRAME_COUNT;
Log.i(TAG, "Using " + refreshNsec + "ns as refresh rate");
// Run tests with times specified, at 1.3x, 1x, 1/2x, and 1/4x speed.
//
// One particular device is overly aggressive at reducing clock frequencies, and it
// will slow things to the point where we can't push frames quickly enough in the
// faster test. By adding an artificial workload in a second thread we can make the
// system run faster. (This could have a detrimental effect on a single-core CPU,
// so it's a no-op there.)
CpuWaster cpuWaster = new CpuWaster();
try {
cpuWaster.start();
// Tests with mult < 1.0f are flaky, for two reasons:
//
// (a) They assume that the GPU can render the test scene in less than mult*refreshNsec.
// It's a simple scene, but CTS/CDD don't currently require being able to do more
// than a full-screen clear in refreshNsec.
//
// (b) More importantly, it assumes that the only rate-limiting happening is
// backpressure from the buffer queue. If the EGL implementation is doing its own
// rate-limiting (to limit the amount of work queued to the GPU at any time), then
// depending on how that's implemented the buffer queue may not have two frames
// pending very often. So the consumer won't be able to drop many frames, and the
// throughput won't be much better than with mult=1.0.
//
// runThroughputTest(output, refreshNsec, 0.75f);
cpuWaster.stop();
runThroughputTest(output, refreshNsec, 1.0f);
runThroughputTest(output, refreshNsec, 2.0f);
runThroughputTest(output, refreshNsec, 4.0f);
} finally {
cpuWaster.stop();
}
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, float mult) {
Log.d(TAG, "runThroughputTest: " + mult);
// Sleep briefly. This is strangely necessary on some devices to allow the GPU to
// catch up (b/10898363). It also provides an easily-visible break in the systrace
// output.
try { Thread.sleep(50); }
catch (InterruptedException ignored) {}
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.0f) {
showNsec = startNsec + (long) (frameNum * frameTimeNsec * mult);
}
drawFrame(frameNum, mult);
output.setPresentationTime(showNsec);
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 = (long) (frameTimeNsec * FRAME_COUNT * mult);
long deltaNsec = Math.abs(expectedNsec - actualNsec);
double delta = (double) deltaNsec / expectedNsec;
final double MAX_DELTA = 0.05;
if (delta > MAX_DELTA) {
throw new RuntimeException("Time delta exceeds tolerance (" + MAX_DELTA +
"): mult=" + mult + ": expected=" + expectedNsec +
" actual=" + actualNsec + " p=" + delta);
} 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
*/
public void suppressed_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, float 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");
}
/**
* Wastes CPU time.
* <p>
* The start() and stop() methods must be called from the same thread.
*/
private static class CpuWaster {
volatile boolean mRunning = false;
public void start() {
if (mRunning) {
throw new IllegalStateException("already running");
}
if (Runtime.getRuntime().availableProcessors() < 2) {
return;
}
mRunning = true;
new Thread("Stupid") {
@Override
public void run() {
while (mRunning) { /* spin! */ }
}
}.start();
// sleep briefly while the system re-evaluates its load (might want to spin)
try { Thread.sleep(10); }
catch (InterruptedException ignored) {}
}
public void stop() {
if (mRunning) {
mRunning = false;
// give the system a chance to slow back down
try { Thread.sleep(10); }
catch (InterruptedException ignored) {}
}
}
}
}