1 /*
2  * Copyright (C) 2018 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 package android.gameperformance;
17 
18 import java.util.ArrayList;
19 import java.util.List;
20 
21 import android.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.PixelFormat;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.os.Trace;
28 import android.view.Surface;
29 import android.view.SurfaceHolder;
30 import android.view.SurfaceView;
31 
32 /**
33  * Minimal SurfaceView that sends buffer on request.
34  */
35 public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
36     // Tag for trace when buffer is requested.
37     public final static String LOCAL_REQUEST_BUFFER = "localRequestBuffer";
38     // Tag for trace when buffer is posted.
39     public final static String LOCAL_POST_BUFFER = "localPostBuffer";
40 
41     private final Object mSurfaceLock = new Object();
42     // Keeps frame times. Used to calculate fps.
43     private List<Long> mFrameTimes;
44     // Surface to send.
45     private Surface mSurface;
46     private Handler mHandler;
47 
48     private Runnable mInvalidateSurfaceTask = new Runnable() {
49         @Override
50         public void run() {
51             synchronized (mSurfaceLock) {
52                 if (mSurface == null) {
53                     return;
54                 }
55                 invalidateSurface(true, true);
56                 mHandler.post(this);
57             }
58         }
59     };
60 
CustomSurfaceView(Context context)61     public CustomSurfaceView(Context context) {
62         super(context);
63         mFrameTimes = new ArrayList<Long>();
64         getHolder().addCallback(this);
65         getHolder().setFormat(PixelFormat.OPAQUE);
66 
67         HandlerThread thread = new HandlerThread("SurfaceInvalidator");
68         thread.start();
69         mHandler = new Handler(thread.getLooper());
70     }
71 
72     /**
73      * Resets frame times in order to calculate fps for different test pass.
74      */
resetFrameTimes()75     public void resetFrameTimes() {
76         synchronized (mSurfaceLock) {
77             mFrameTimes.clear();
78         }
79     }
80 
81     /**
82      * Returns current fps based on collected frame times.
83      */
getFps()84     public double getFps() {
85         synchronized (mSurfaceLock) {
86             if (mFrameTimes.size() < 2) {
87                 return 0.0f;
88             }
89             return 1000.0 * mFrameTimes.size() /
90                     (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
91         }
92     }
93 
94     /**
95      * Invalidates surface.
96      * @param traceCalls set to true in case we need register trace calls. Not used for warm-up.
97      * @param drawFps perform drawing current fps on surface to have some payload on surface.
98      */
invalidateSurface(boolean traceCalls, boolean drawFps)99     public void invalidateSurface(boolean traceCalls, boolean drawFps) {
100         synchronized (mSurfaceLock) {
101             if (mSurface == null) {
102                 throw new IllegalStateException("Surface is not ready");
103             }
104             if (traceCalls) {
105                 Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_REQUEST_BUFFER);
106             }
107             Canvas canvas = mSurface.lockHardwareCanvas();
108             if (traceCalls) {
109                 Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
110             }
111 
112             if (drawFps) {
113                 int textSize = canvas.getHeight() / 24;
114                 Paint paint = new Paint();
115                 paint.setTextSize(textSize);
116                 paint.setColor(0xFFFF8040);
117                 canvas.drawARGB(92, 255, 255, 255);
118                 canvas.drawText("FPS: " + String.format("%.2f", getFps()), 10, 300, paint);
119             }
120 
121             if (traceCalls) {
122                 Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_POST_BUFFER);
123             }
124             mSurface.unlockCanvasAndPost(canvas);
125             if (traceCalls) {
126                 Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
127             }
128 
129             mFrameTimes.add(System.currentTimeMillis());
130         }
131     }
132 
133     /**
134      * Wait until surface is created and ready to use or return immediately if surface
135      * already exists.
136      */
waitForSurfaceReady()137     public void waitForSurfaceReady() {
138         synchronized (mSurfaceLock) {
139             if (mSurface == null) {
140                 try {
141                     mSurfaceLock.wait(5000);
142                 } catch(InterruptedException e) {
143                     e.printStackTrace();
144                 }
145             }
146             if (mSurface == null)
147                 throw new IllegalStateException("Surface is not ready.");
148         }
149     }
150 
151     /**
152      * Waits until surface is destroyed or return immediately if surface does not exist.
153      */
waitForSurfaceDestroyed()154     public void waitForSurfaceDestroyed() {
155         synchronized (mSurfaceLock) {
156             if (mSurface != null) {
157                 try {
158                     mSurfaceLock.wait(5000);
159                 } catch(InterruptedException e) {
160                 }
161             }
162             if (mSurface != null)
163                 throw new IllegalStateException("Surface still exists.");
164         }
165     }
166 
167 
168     @Override
surfaceCreated(SurfaceHolder holder)169     public void surfaceCreated(SurfaceHolder holder) {
170     }
171 
172     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)173     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
174         // This method is always called at least once, after surfaceCreated.
175         synchronized (mSurfaceLock) {
176             mSurface = holder.getSurface();
177             mSurfaceLock.notify();
178             mHandler.post(mInvalidateSurfaceTask);
179         }
180     }
181 
182     @Override
surfaceDestroyed(SurfaceHolder holder)183     public void surfaceDestroyed(SurfaceHolder holder) {
184         synchronized (mSurfaceLock) {
185             mHandler.removeCallbacks(mInvalidateSurfaceTask);
186             mSurface = null;
187             mSurfaceLock.notify();
188         }
189     }
190 }
191