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