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