1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.misc.cts;
18 
19 import android.media.cts.InputSurface;
20 import android.media.cts.MediaStubActivity;
21 import android.opengl.GLES20;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.os.Trace;
26 import android.platform.test.annotations.AppModeFull;
27 import android.test.ActivityInstrumentationTestCase2;
28 import android.util.Log;
29 import android.view.Choreographer;
30 import android.view.SurfaceHolder;
31 
32 import com.android.compatibility.common.util.FrameworkSpecificTest;
33 import com.android.compatibility.common.util.NonMainlineTest;
34 
35 
36 /**
37  * Tests synchronized frame presentation.
38  *
39  * SurfaceFlinger allows a "desired presentation time" value to be passed along with buffers of
40  * data.  This exercises that feature.
41  */
42 @FrameworkSpecificTest
43 @NonMainlineTest
44 @AppModeFull(reason = "TODO: evaluate and port to instant")
45 public class PresentationSyncTest extends ActivityInstrumentationTestCase2<MediaStubActivity>
46         implements SurfaceHolder.Callback {
47     private static final String TAG = "PresentationSyncTest";
48     private static final boolean VERBOSE = false;           // lots of logging
49     private static final int FRAME_COUNT = 512;             // ~10 sec @ 60fps
50 
51     // message values
52     private static final int START_TEST = 0;
53     private static final int END_TEST = 1;
54 
55     // width and height of the Surface we're given to draw on
56     private int mWidth;
57     private int mHeight;
58 
PresentationSyncTest()59     public PresentationSyncTest() {
60         super(MediaStubActivity.class);
61     }
62 
63     /**
64      * Tests whether the output frame rate can be limited by the presentation time.
65      * <p>
66      * Generates and displays the same series of images three times.  The first run uses "now"
67      * as the desired presentation time to establish an estimate of the refresh time.  Later
68      * runs set the presentation time to (start_time + frame_number * refresh_time * multiplier),
69      * with the expectation that a multiplier of 2 will cause the animation to render at
70      * half speed.
71      * <p>
72      * This test does not use Choreographer.  The longer the test runs, the farther out of
73      * phase the test will become with respect to the actual vsync timing.
74      * <p>
75      * Setting the presentation time for a frame is most easily done through an EGL extension,
76      * so we render each frame through GL.
77      *
78      * @throws Exception
79      */
testThroughput()80     public void testThroughput() throws Exception {
81         // Get the Surface from the SurfaceView.
82         // TODO: is it safe to assume that it's ready?
83         SurfaceHolder holder = getActivity().getSurfaceHolder();
84         holder.addCallback(this);
85 
86         // We use the width/height to render a simple series of patterns.  If we get this
87         // wrong it shouldn't really matter -- some driver optimizations might make things
88         // faster, but it shouldn't affect how long it takes the frame to be displayed.
89         //
90         // We can get this from the View or from the EGLSurface.  We don't have easy direct
91         // access to any of those things, so just ask our InputSurface to get it from EGL,
92         // since that's where we're drawing.
93         //
94         // Note: InputSurface was intended for a different purpose, but it's 99% right for our
95         // needs.  Maybe rename it to "RecordableSurface"?  Or trivially wrap it with a
96         // subclass that suppresses the EGL_RECORDABLE_ANDROID flag?
97         InputSurface output = new InputSurface(holder.getSurface());
98         mWidth = output.getWidth();
99         mHeight = output.getHeight();
100         Log.d(TAG, "Surface w=" + mWidth + " h=" + mHeight);
101         output.makeCurrent();
102 
103         // Run a test with no presentation times specified.  Assuming nothing else is
104         // fighting us for resources, all frames should display as quickly as possible,
105         // and we can estimate the refresh rate of the device.
106         long baseTimeNsec = runThroughputTest(output, 0L, -1.0f);
107         long refreshNsec = baseTimeNsec / FRAME_COUNT;
108         Log.i(TAG, "Using " + refreshNsec + "ns as refresh rate");
109 
110         // Run tests with times specified, at 1.3x, 1x, 1/2x, and 1/4x speed.
111         //
112         // One particular device is overly aggressive at reducing clock frequencies, and it
113         // will slow things to the point where we can't push frames quickly enough in the
114         // faster test.  By adding an artificial workload in a second thread we can make the
115         // system run faster.  (This could have a detrimental effect on a single-core CPU,
116         // so it's a no-op there.)
117         CpuWaster cpuWaster = new CpuWaster();
118         try {
119             cpuWaster.start();
120             // Tests with mult < 1.0f are flaky, for two reasons:
121             //
122             // (a) They assume that the GPU can render the test scene in less than mult*refreshNsec.
123             //     It's a simple scene, but CTS/CDD don't currently require being able to do more
124             //     than a full-screen clear in refreshNsec.
125             //
126             // (b) More importantly, it assumes that the only rate-limiting happening is
127             //     backpressure from the buffer queue. If the EGL implementation is doing its own
128             //     rate-limiting (to limit the amount of work queued to the GPU at any time), then
129             //     depending on how that's implemented the buffer queue may not have two frames
130             //     pending very often. So the consumer won't be able to drop many frames, and the
131             //     throughput won't be much better than with mult=1.0.
132             //
133             // runThroughputTest(output, refreshNsec, 0.75f);
134             cpuWaster.stop();
135             runThroughputTest(output, refreshNsec, 1.0f);
136             runThroughputTest(output, refreshNsec, 2.0f);
137             runThroughputTest(output, refreshNsec, 4.0f);
138         } finally {
139             cpuWaster.stop();
140         }
141 
142         output.release();
143     }
144 
145     /**
146      * Runs the throughput test on the provided surface with the specified time values.
147      * <p>
148      * If mult is -1, the test runs in "training" mode, rendering frames as quickly as
149      * possible.  This can be used to establish a baseline.
150      * <p>
151      * @return the test duration, in nanoseconds
152      */
runThroughputTest(InputSurface output, long frameTimeNsec, float mult)153     private long runThroughputTest(InputSurface output, long frameTimeNsec, float mult) {
154         Log.d(TAG, "runThroughputTest: " + mult);
155 
156         // Sleep briefly.  This is strangely necessary on some devices to allow the GPU to
157         // catch up (b/10898363).  It also provides an easily-visible break in the systrace
158         // output.
159         try { Thread.sleep(50); }
160         catch (InterruptedException ignored) {}
161 
162         long startNsec = System.nanoTime();
163         long showNsec = 0;
164 
165         if (true) {
166             // Output a frame that creates a "marker" in the --latency output
167             drawFrame(0, mult);
168             Trace.beginSection("TEST BEGIN");
169             output.swapBuffers();
170             Trace.endSection();
171             startNsec = System.nanoTime();
172         }
173 
174         for (int frameNum = 0; frameNum < FRAME_COUNT; frameNum++) {
175             if (mult != -1.0f) {
176                 showNsec = startNsec + (long) (frameNum * frameTimeNsec * mult);
177             }
178             drawFrame(frameNum, mult);
179             if (mult != -1.0f) {
180                 output.setPresentationTime(showNsec);
181             }
182             Trace.beginSection("swapbuf " + frameNum);
183             output.swapBuffers();
184             Trace.endSection();
185         }
186 
187         long endNsec = System.nanoTime();
188         long actualNsec = endNsec - startNsec;
189 
190         if (mult != -1) {
191             // Some variation is inevitable, but we should be within a few percent of expected.
192             long expectedNsec = (long) (frameTimeNsec * FRAME_COUNT * mult);
193             long deltaNsec = Math.abs(expectedNsec - actualNsec);
194             double delta = (double) deltaNsec / expectedNsec;
195             final double MAX_DELTA = 0.05;
196             if (delta > MAX_DELTA) {
197                 throw new RuntimeException("Time delta exceeds tolerance (" + MAX_DELTA +
198                         "): mult=" + mult + ": expected=" + expectedNsec +
199                         " actual=" + actualNsec + " p=" + delta);
200 
201             } else {
202                 Log.d(TAG, "mult=" + mult + ": expected=" + expectedNsec +
203                         " actual=" + actualNsec + " p=" + delta);
204             }
205         }
206         return endNsec - startNsec;
207     }
208 
209 
210     /**
211      * Exercises the test code, driving it off of Choreographer.  The animation is driven at
212      * full speed, but with rendering requested at a future time.  With each run the distance
213      * into the future is increased.
214      * <p>
215      * Loopers can't be reused once they quit, so it's easiest to create a new thread for
216      * each run.
217      * <p>
218      * (This isn't exactly a test -- it's primarily a way to exercise the code.  Evaluate the
219      * results with "dumpsys SurfaceFlinger --latency SurfaceView" for each multiplier.
220      * The idea is to see frames where the desired-present is as close as possible to the
221      * actual-present, while still minimizing frame-ready.  If we go too far into the future
222      * the BufferQueue will start to back up.)
223      * <p>
224      * @throws Exception
225      */
suppressed_testChoreographed()226     public void suppressed_testChoreographed() throws Throwable {
227         // Get the Surface from the SurfaceView.
228         // TODO: is it safe to assume that it's ready?
229         SurfaceHolder holder = getActivity().getSurfaceHolder();
230         holder.addCallback(this);
231 
232         InputSurface output = new InputSurface(holder.getSurface());
233         mWidth = output.getWidth();
234         mHeight = output.getHeight();
235         Log.d(TAG, "Surface w=" + mWidth + " h=" + mHeight);
236 
237         for (int i = 1; i < 5; i++) {
238             ChoreographedWrapper.runTest(this, output, i);
239         }
240 
241         output.release();
242     }
243 
244     /**
245      * Shifts the test to a new thread, so we can manage our own Looper.  Any exception
246      * thrown on the new thread is propagated to the caller.
247      */
248     private static class ChoreographedWrapper implements Runnable {
249         private final PresentationSyncTest mTest;
250         private final InputSurface mOutput;
251         private final int mFrameDelay;
252         private Throwable mThrowable;
253 
ChoreographedWrapper(PresentationSyncTest test, InputSurface output, int frameDelay)254         private ChoreographedWrapper(PresentationSyncTest test, InputSurface output,
255                 int frameDelay) {
256             mTest = test;
257             mOutput = output;
258             mFrameDelay = frameDelay;
259         }
260 
261         @Override
run()262         public void run() {
263             try {
264                 mTest.runChoreographedTest(mOutput, mFrameDelay);
265             } catch (Throwable th) {
266                 mThrowable = th;
267             }
268         }
269 
270         /** Entry point. */
runTest(PresentationSyncTest obj, InputSurface output, int frameDelay)271         public static void runTest(PresentationSyncTest obj, InputSurface output,
272                 int frameDelay) throws Throwable {
273             ChoreographedWrapper wrapper = new ChoreographedWrapper(obj, output, frameDelay);
274             Thread th = new Thread(wrapper, "sync test");
275             th.start();
276             th.join();
277             if (wrapper.mThrowable != null) {
278                 throw wrapper.mThrowable;
279             }
280         }
281     }
282 
283     /**
284      * Runs the test, driven by callbacks from the Looper we define here.
285      */
runChoreographedTest(InputSurface output, int frameDelay)286     private void runChoreographedTest(InputSurface output, int frameDelay) {
287         Log.d(TAG, "runChoreographedTest");
288 
289         output.makeCurrent();
290         final ChoRunner chore = new ChoRunner(output);
291 
292         Looper.prepare();
293         Handler handler = new Handler() {
294             @Override
295             public void handleMessage(Message msg) {
296                 switch (msg.what) {
297                     case START_TEST:
298                         Log.d(TAG, "Starting test");
299                         chore.start(this, msg.arg1 /*frameDelay*/);
300                         break;
301                     case END_TEST:
302                         Log.d(TAG, "Ending test");
303                         Looper.myLooper().quitSafely();
304                         break;
305                     default:
306                         Log.d(TAG, "unknown message " + msg.what);
307                         break;
308                 }
309             }
310         };
311 
312         handler.sendMessage(Message.obtain(handler, START_TEST, frameDelay, 0));
313 
314         Log.d(TAG, "looping (frameDelay=" + frameDelay + ")");
315         long startNanos = System.nanoTime();
316         Trace.beginSection("TEST BEGIN fd=" + frameDelay);
317         Looper.loop();
318         Trace.endSection();
319         long durationNanos = System.nanoTime() - startNanos;
320         Log.d(TAG, "loop exiting after " + durationNanos +
321                 " (" + durationNanos / FRAME_COUNT + "ns)");
322 
323         output.makeUnCurrent();
324     }
325 
326 
327     private class ChoRunner implements Choreographer.FrameCallback {
328         private final InputSurface mOutput;
329         private int mFrameDelay;
330         private Handler mHandler;
331         private int mCurFrame;
332         private Choreographer mChocho;
333         private long mPrevFrameTimeNanos;
334         private long mFrameDiff;
335 
ChoRunner(InputSurface output)336         public ChoRunner(InputSurface output) {
337             mOutput = output;
338         }
339 
start(Handler handler, int frameDelay)340         public void start(Handler handler, int frameDelay) {
341             mHandler = handler;
342             mFrameDelay = frameDelay;
343 
344             mCurFrame = 0;
345             mChocho = Choreographer.getInstance();
346             mChocho.postFrameCallback(this);
347         }
348 
349         @Override
doFrame(long frameTimeNanos)350         public void doFrame(long frameTimeNanos) {
351             if (mPrevFrameTimeNanos != 0) {
352                 // Update our vsync rate guess every frame so that, if we start with a
353                 // stutter, we don't carry it for the whole test.
354                 assertTrue(frameTimeNanos > mPrevFrameTimeNanos);
355                 long prevDiff = frameTimeNanos - mPrevFrameTimeNanos;
356                 if (mFrameDiff == 0 || mFrameDiff > prevDiff) {
357                     mFrameDiff = prevDiff;
358                     Log.d(TAG, "refresh rate approx " + mFrameDiff + "ns");
359                 }
360 
361                 // If the current diff is >= 2x the expected frame time diff, we stuttered
362                 // and need to drop a frame.  (We might even need to drop more than one
363                 // frame; ignoring that for now.)
364                 if (prevDiff > mFrameDiff * 1.9) {
365                     Log.d(TAG, "skip " + mCurFrame + " diff=" + prevDiff);
366                     mCurFrame++;
367                 }
368             }
369             mPrevFrameTimeNanos = frameTimeNanos;
370 
371             if (mFrameDiff != 0) {
372                 // set desired display time to N frames in the future, rather than ASAP.
373                 //
374                 // Note this is a "don't open until Xmas" feature.  If vsyncs are happening
375                 // at times T1, T2, T3, and we want the frame to appear onscreen when the
376                 // buffers flip at T2, then we can theoretically request any time value
377                 // in [T1, T2).
378                 mOutput.setPresentationTime(frameTimeNanos + (mFrameDiff * mFrameDelay));
379             }
380 
381             drawFrame(mCurFrame, mFrameDelay);
382             Trace.beginSection("swapbuf " + mCurFrame);
383             mOutput.swapBuffers();
384             Trace.endSection();
385 
386             if (++mCurFrame < FRAME_COUNT) {
387                 mChocho.postFrameCallback(this);
388             } else {
389                 mHandler.sendMessage(Message.obtain(mHandler, END_TEST));
390             }
391         }
392     }
393 
394     /**
395      * Draws a frame with GLES in the current context.
396      */
drawFrame(int num, float mult)397     private void drawFrame(int num, float mult) {
398         num %= 64;
399         float colorVal;
400 
401         if (mult > 0) {
402             colorVal = 1.0f / mult;
403         } else {
404             colorVal = 0.1f;
405         }
406 
407         int startX, startY;
408         startX = (num % 16) * (mWidth / 16);
409         startY = (num / 16) * (mHeight / 4);
410         if ((num >= 16 && num < 32) || (num >= 48)) {
411             // reverse direction
412             startX = (mWidth - mWidth/16) - startX;
413         }
414 
415         // clear screen
416         GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
417         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
418 
419         // draw rect
420         GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
421         GLES20.glScissor(startX, startY, mWidth / 16, mHeight / 4);
422         GLES20.glClearColor(colorVal, 1 - colorVal, 0.0f, 1.0f);
423         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
424         GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
425     }
426 
427     @Override
surfaceCreated(SurfaceHolder holder)428     public void surfaceCreated(SurfaceHolder holder) {
429         Log.d(TAG, "surfaceCreated");
430     }
431 
432     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)433     public void surfaceChanged(SurfaceHolder holder, int format, int width,
434             int height) {
435         // This doesn't seem to happen in practice with current test framework -- Surface is
436         // already created before we start, and the orientation is locked.
437         Log.d(TAG, "surfaceChanged f=" + format + " w=" + width + " h=" + height);
438         mWidth = width;
439         mHeight = height;
440     }
441 
442     @Override
surfaceDestroyed(SurfaceHolder holder)443     public void surfaceDestroyed(SurfaceHolder holder) {
444         Log.d(TAG, "surfaceDestroyed");
445     }
446 
447 
448     /**
449      * Wastes CPU time.
450      * <p>
451      * The start() and stop() methods must be called from the same thread.
452      */
453     private static class CpuWaster {
454         volatile boolean mRunning = false;
455 
start()456         public void start() {
457             if (mRunning) {
458                 throw new IllegalStateException("already running");
459             }
460 
461             if (Runtime.getRuntime().availableProcessors() < 2) {
462                 return;
463             }
464 
465             mRunning = true;
466 
467             new Thread("Stupid") {
468                 @Override
469                 public void run() {
470                     while (mRunning) { /* spin! */ }
471                 }
472             }.start();
473 
474             // sleep briefly while the system re-evaluates its load (might want to spin)
475             try { Thread.sleep(10); }
476             catch (InterruptedException ignored) {}
477         }
478 
stop()479         public void stop() {
480             if (mRunning) {
481                 mRunning = false;
482 
483                 // give the system a chance to slow back down
484                 try { Thread.sleep(10); }
485                 catch (InterruptedException ignored) {}
486             }
487         }
488     }
489 }
490