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