Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko.gfx; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.GeckoAppShell; |
michael@0 | 9 | import org.mozilla.gecko.R; |
michael@0 | 10 | import org.mozilla.gecko.Tab; |
michael@0 | 11 | import org.mozilla.gecko.Tabs; |
michael@0 | 12 | import org.mozilla.gecko.gfx.Layer.RenderContext; |
michael@0 | 13 | import org.mozilla.gecko.gfx.RenderTask; |
michael@0 | 14 | import org.mozilla.gecko.mozglue.DirectBufferAllocator; |
michael@0 | 15 | |
michael@0 | 16 | import android.content.Context; |
michael@0 | 17 | import android.content.SharedPreferences; |
michael@0 | 18 | import android.content.res.Resources; |
michael@0 | 19 | import android.graphics.Bitmap; |
michael@0 | 20 | import android.graphics.Canvas; |
michael@0 | 21 | import android.graphics.Color; |
michael@0 | 22 | import android.graphics.Matrix; |
michael@0 | 23 | import android.graphics.PointF; |
michael@0 | 24 | import android.graphics.Rect; |
michael@0 | 25 | import android.graphics.RectF; |
michael@0 | 26 | import android.opengl.GLES20; |
michael@0 | 27 | import android.os.SystemClock; |
michael@0 | 28 | import android.util.Log; |
michael@0 | 29 | import org.mozilla.gecko.mozglue.JNITarget; |
michael@0 | 30 | |
michael@0 | 31 | import java.nio.ByteBuffer; |
michael@0 | 32 | import java.nio.ByteOrder; |
michael@0 | 33 | import java.nio.FloatBuffer; |
michael@0 | 34 | import java.nio.IntBuffer; |
michael@0 | 35 | import java.util.concurrent.CopyOnWriteArrayList; |
michael@0 | 36 | |
michael@0 | 37 | import javax.microedition.khronos.egl.EGLConfig; |
michael@0 | 38 | |
michael@0 | 39 | /** |
michael@0 | 40 | * The layer renderer implements the rendering logic for a layer view. |
michael@0 | 41 | */ |
michael@0 | 42 | public class LayerRenderer implements Tabs.OnTabsChangedListener { |
michael@0 | 43 | private static final String LOGTAG = "GeckoLayerRenderer"; |
michael@0 | 44 | private static final String PROFTAG = "GeckoLayerRendererProf"; |
michael@0 | 45 | |
michael@0 | 46 | /* |
michael@0 | 47 | * The amount of time a frame is allowed to take to render before we declare it a dropped |
michael@0 | 48 | * frame. |
michael@0 | 49 | */ |
michael@0 | 50 | private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */ |
michael@0 | 51 | |
michael@0 | 52 | private static final int FRAME_RATE_METER_WIDTH = 128; |
michael@0 | 53 | private static final int FRAME_RATE_METER_HEIGHT = 32; |
michael@0 | 54 | |
michael@0 | 55 | private static final long NANOS_PER_MS = 1000000; |
michael@0 | 56 | private static final int NANOS_PER_SECOND = 1000000000; |
michael@0 | 57 | |
michael@0 | 58 | private final LayerView mView; |
michael@0 | 59 | private TextLayer mFrameRateLayer; |
michael@0 | 60 | private final ScrollbarLayer mHorizScrollLayer; |
michael@0 | 61 | private final ScrollbarLayer mVertScrollLayer; |
michael@0 | 62 | private final FadeRunnable mFadeRunnable; |
michael@0 | 63 | private ByteBuffer mCoordByteBuffer; |
michael@0 | 64 | private FloatBuffer mCoordBuffer; |
michael@0 | 65 | private RenderContext mLastPageContext; |
michael@0 | 66 | private int mMaxTextureSize; |
michael@0 | 67 | private int mBackgroundColor; |
michael@0 | 68 | private int mOverscrollColor; |
michael@0 | 69 | |
michael@0 | 70 | private long mLastFrameTime; |
michael@0 | 71 | private final CopyOnWriteArrayList<RenderTask> mTasks; |
michael@0 | 72 | |
michael@0 | 73 | private CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>(); |
michael@0 | 74 | |
michael@0 | 75 | // Dropped frames display |
michael@0 | 76 | private int[] mFrameTimings; |
michael@0 | 77 | private int mCurrentFrame, mFrameTimingsSum, mDroppedFrames; |
michael@0 | 78 | |
michael@0 | 79 | // Render profiling output |
michael@0 | 80 | private int mFramesRendered; |
michael@0 | 81 | private float mCompleteFramesRendered; |
michael@0 | 82 | private boolean mProfileRender; |
michael@0 | 83 | private long mProfileOutputTime; |
michael@0 | 84 | |
michael@0 | 85 | private IntBuffer mPixelBuffer; |
michael@0 | 86 | |
michael@0 | 87 | // Used by GLES 2.0 |
michael@0 | 88 | private int mProgram; |
michael@0 | 89 | private int mPositionHandle; |
michael@0 | 90 | private int mTextureHandle; |
michael@0 | 91 | private int mSampleHandle; |
michael@0 | 92 | private int mTMatrixHandle; |
michael@0 | 93 | |
michael@0 | 94 | // column-major matrix applied to each vertex to shift the viewport from |
michael@0 | 95 | // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by |
michael@0 | 96 | // a factor of 2 to fill up the screen |
michael@0 | 97 | public static final float[] DEFAULT_TEXTURE_MATRIX = { |
michael@0 | 98 | 2.0f, 0.0f, 0.0f, 0.0f, |
michael@0 | 99 | 0.0f, 2.0f, 0.0f, 0.0f, |
michael@0 | 100 | 0.0f, 0.0f, 2.0f, 0.0f, |
michael@0 | 101 | -1.0f, -1.0f, 0.0f, 1.0f |
michael@0 | 102 | }; |
michael@0 | 103 | |
michael@0 | 104 | private static final int COORD_BUFFER_SIZE = 20; |
michael@0 | 105 | |
michael@0 | 106 | // The shaders run on the GPU directly, the vertex shader is only applying the |
michael@0 | 107 | // matrix transform detailed above |
michael@0 | 108 | |
michael@0 | 109 | // Note we flip the y-coordinate in the vertex shader from a |
michael@0 | 110 | // coordinate system with (0,0) in the top left to one with (0,0) in |
michael@0 | 111 | // the bottom left. |
michael@0 | 112 | |
michael@0 | 113 | public static final String DEFAULT_VERTEX_SHADER = |
michael@0 | 114 | "uniform mat4 uTMatrix;\n" + |
michael@0 | 115 | "attribute vec4 vPosition;\n" + |
michael@0 | 116 | "attribute vec2 aTexCoord;\n" + |
michael@0 | 117 | "varying vec2 vTexCoord;\n" + |
michael@0 | 118 | "void main() {\n" + |
michael@0 | 119 | " gl_Position = uTMatrix * vPosition;\n" + |
michael@0 | 120 | " vTexCoord.x = aTexCoord.x;\n" + |
michael@0 | 121 | " vTexCoord.y = 1.0 - aTexCoord.y;\n" + |
michael@0 | 122 | "}\n"; |
michael@0 | 123 | |
michael@0 | 124 | // We use highp because the screenshot textures |
michael@0 | 125 | // we use are large and we stretch them alot |
michael@0 | 126 | // so we need all the precision we can get. |
michael@0 | 127 | // Unfortunately, highp is not required by ES 2.0 |
michael@0 | 128 | // so on GPU's like Mali we end up getting mediump |
michael@0 | 129 | public static final String DEFAULT_FRAGMENT_SHADER = |
michael@0 | 130 | "precision highp float;\n" + |
michael@0 | 131 | "varying vec2 vTexCoord;\n" + |
michael@0 | 132 | "uniform sampler2D sTexture;\n" + |
michael@0 | 133 | "void main() {\n" + |
michael@0 | 134 | " gl_FragColor = texture2D(sTexture, vTexCoord);\n" + |
michael@0 | 135 | "}\n"; |
michael@0 | 136 | |
michael@0 | 137 | public LayerRenderer(LayerView view) { |
michael@0 | 138 | mView = view; |
michael@0 | 139 | try { |
michael@0 | 140 | mOverscrollColor = view.getContext().getResources().getColor(R.color.background_normal); |
michael@0 | 141 | } catch (Resources.NotFoundException nfe) { mOverscrollColor = Color.BLACK; } |
michael@0 | 142 | |
michael@0 | 143 | Bitmap scrollbarImage = view.getScrollbarImage(); |
michael@0 | 144 | IntSize size = new IntSize(scrollbarImage.getWidth(), scrollbarImage.getHeight()); |
michael@0 | 145 | scrollbarImage = expandCanvasToPowerOfTwo(scrollbarImage, size); |
michael@0 | 146 | |
michael@0 | 147 | mTasks = new CopyOnWriteArrayList<RenderTask>(); |
michael@0 | 148 | mLastFrameTime = System.nanoTime(); |
michael@0 | 149 | |
michael@0 | 150 | mVertScrollLayer = new ScrollbarLayer(this, scrollbarImage, size, true); |
michael@0 | 151 | mHorizScrollLayer = new ScrollbarLayer(this, diagonalFlip(scrollbarImage), new IntSize(size.height, size.width), false); |
michael@0 | 152 | mFadeRunnable = new FadeRunnable(); |
michael@0 | 153 | |
michael@0 | 154 | mFrameTimings = new int[60]; |
michael@0 | 155 | mCurrentFrame = mFrameTimingsSum = mDroppedFrames = 0; |
michael@0 | 156 | |
michael@0 | 157 | // Initialize the FloatBuffer that will be used to store all vertices and texture |
michael@0 | 158 | // coordinates in draw() commands. |
michael@0 | 159 | mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4); |
michael@0 | 160 | mCoordByteBuffer.order(ByteOrder.nativeOrder()); |
michael@0 | 161 | mCoordBuffer = mCoordByteBuffer.asFloatBuffer(); |
michael@0 | 162 | |
michael@0 | 163 | Tabs.registerOnTabsChangedListener(this); |
michael@0 | 164 | } |
michael@0 | 165 | |
michael@0 | 166 | private Bitmap expandCanvasToPowerOfTwo(Bitmap image, IntSize size) { |
michael@0 | 167 | IntSize potSize = size.nextPowerOfTwo(); |
michael@0 | 168 | if (size.equals(potSize)) { |
michael@0 | 169 | return image; |
michael@0 | 170 | } |
michael@0 | 171 | // make the bitmap size a power-of-two in both dimensions if it's not already. |
michael@0 | 172 | Bitmap potImage = Bitmap.createBitmap(potSize.width, potSize.height, image.getConfig()); |
michael@0 | 173 | new Canvas(potImage).drawBitmap(image, new Matrix(), null); |
michael@0 | 174 | return potImage; |
michael@0 | 175 | } |
michael@0 | 176 | |
michael@0 | 177 | private Bitmap diagonalFlip(Bitmap image) { |
michael@0 | 178 | Matrix rotation = new Matrix(); |
michael@0 | 179 | rotation.setValues(new float[] { 0, 1, 0, 1, 0, 0, 0, 0, 1 }); // transform (x,y) into (y,x) |
michael@0 | 180 | Bitmap rotated = Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), rotation, true); |
michael@0 | 181 | return rotated; |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | public void destroy() { |
michael@0 | 185 | DirectBufferAllocator.free(mCoordByteBuffer); |
michael@0 | 186 | mCoordByteBuffer = null; |
michael@0 | 187 | mCoordBuffer = null; |
michael@0 | 188 | mHorizScrollLayer.destroy(); |
michael@0 | 189 | mVertScrollLayer.destroy(); |
michael@0 | 190 | if (mFrameRateLayer != null) { |
michael@0 | 191 | mFrameRateLayer.destroy(); |
michael@0 | 192 | } |
michael@0 | 193 | Tabs.unregisterOnTabsChangedListener(this); |
michael@0 | 194 | } |
michael@0 | 195 | |
michael@0 | 196 | void onSurfaceCreated(EGLConfig config) { |
michael@0 | 197 | checkMonitoringEnabled(); |
michael@0 | 198 | createDefaultProgram(); |
michael@0 | 199 | activateDefaultProgram(); |
michael@0 | 200 | } |
michael@0 | 201 | |
michael@0 | 202 | public void createDefaultProgram() { |
michael@0 | 203 | int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER); |
michael@0 | 204 | int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER); |
michael@0 | 205 | |
michael@0 | 206 | mProgram = GLES20.glCreateProgram(); |
michael@0 | 207 | GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program |
michael@0 | 208 | GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program |
michael@0 | 209 | GLES20.glLinkProgram(mProgram); // creates OpenGL program executables |
michael@0 | 210 | |
michael@0 | 211 | // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members. |
michael@0 | 212 | mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); |
michael@0 | 213 | mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord"); |
michael@0 | 214 | mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture"); |
michael@0 | 215 | mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix"); |
michael@0 | 216 | |
michael@0 | 217 | int maxTextureSizeResult[] = new int[1]; |
michael@0 | 218 | GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0); |
michael@0 | 219 | mMaxTextureSize = maxTextureSizeResult[0]; |
michael@0 | 220 | } |
michael@0 | 221 | |
michael@0 | 222 | // Activates the shader program. |
michael@0 | 223 | public void activateDefaultProgram() { |
michael@0 | 224 | // Add the program to the OpenGL environment |
michael@0 | 225 | GLES20.glUseProgram(mProgram); |
michael@0 | 226 | |
michael@0 | 227 | // Set the transformation matrix |
michael@0 | 228 | GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0); |
michael@0 | 229 | |
michael@0 | 230 | // Enable the arrays from which we get the vertex and texture coordinates |
michael@0 | 231 | GLES20.glEnableVertexAttribArray(mPositionHandle); |
michael@0 | 232 | GLES20.glEnableVertexAttribArray(mTextureHandle); |
michael@0 | 233 | |
michael@0 | 234 | GLES20.glUniform1i(mSampleHandle, 0); |
michael@0 | 235 | |
michael@0 | 236 | // TODO: Move these calls into a separate deactivate() call that is called after the |
michael@0 | 237 | // underlay and overlay are rendered. |
michael@0 | 238 | } |
michael@0 | 239 | |
michael@0 | 240 | // Deactivates the shader program. This must be done to avoid crashes after returning to the |
michael@0 | 241 | // Gecko C++ compositor from Java. |
michael@0 | 242 | public void deactivateDefaultProgram() { |
michael@0 | 243 | GLES20.glDisableVertexAttribArray(mTextureHandle); |
michael@0 | 244 | GLES20.glDisableVertexAttribArray(mPositionHandle); |
michael@0 | 245 | GLES20.glUseProgram(0); |
michael@0 | 246 | } |
michael@0 | 247 | |
michael@0 | 248 | public int getMaxTextureSize() { |
michael@0 | 249 | return mMaxTextureSize; |
michael@0 | 250 | } |
michael@0 | 251 | |
michael@0 | 252 | public void postRenderTask(RenderTask aTask) { |
michael@0 | 253 | mTasks.add(aTask); |
michael@0 | 254 | mView.requestRender(); |
michael@0 | 255 | } |
michael@0 | 256 | |
michael@0 | 257 | public void removeRenderTask(RenderTask aTask) { |
michael@0 | 258 | mTasks.remove(aTask); |
michael@0 | 259 | } |
michael@0 | 260 | |
michael@0 | 261 | private void runRenderTasks(CopyOnWriteArrayList<RenderTask> tasks, boolean after, long frameStartTime) { |
michael@0 | 262 | for (RenderTask task : tasks) { |
michael@0 | 263 | if (task.runAfter != after) { |
michael@0 | 264 | continue; |
michael@0 | 265 | } |
michael@0 | 266 | |
michael@0 | 267 | boolean stillRunning = task.run(frameStartTime - mLastFrameTime, frameStartTime); |
michael@0 | 268 | |
michael@0 | 269 | // Remove the task from the list if its finished |
michael@0 | 270 | if (!stillRunning) { |
michael@0 | 271 | tasks.remove(task); |
michael@0 | 272 | } |
michael@0 | 273 | } |
michael@0 | 274 | } |
michael@0 | 275 | |
michael@0 | 276 | public void addLayer(Layer layer) { |
michael@0 | 277 | synchronized (mExtraLayers) { |
michael@0 | 278 | if (mExtraLayers.contains(layer)) { |
michael@0 | 279 | mExtraLayers.remove(layer); |
michael@0 | 280 | } |
michael@0 | 281 | |
michael@0 | 282 | mExtraLayers.add(layer); |
michael@0 | 283 | } |
michael@0 | 284 | } |
michael@0 | 285 | |
michael@0 | 286 | public void removeLayer(Layer layer) { |
michael@0 | 287 | synchronized (mExtraLayers) { |
michael@0 | 288 | mExtraLayers.remove(layer); |
michael@0 | 289 | } |
michael@0 | 290 | } |
michael@0 | 291 | |
michael@0 | 292 | private void printCheckerboardStats() { |
michael@0 | 293 | Log.d(PROFTAG, "Frames rendered over last 1000ms: " + mCompleteFramesRendered + "/" + mFramesRendered); |
michael@0 | 294 | mFramesRendered = 0; |
michael@0 | 295 | mCompleteFramesRendered = 0; |
michael@0 | 296 | } |
michael@0 | 297 | |
michael@0 | 298 | /** Used by robocop for testing purposes. Not for production use! */ |
michael@0 | 299 | IntBuffer getPixels() { |
michael@0 | 300 | IntBuffer pixelBuffer = IntBuffer.allocate(mView.getWidth() * mView.getHeight()); |
michael@0 | 301 | synchronized (pixelBuffer) { |
michael@0 | 302 | mPixelBuffer = pixelBuffer; |
michael@0 | 303 | mView.requestRender(); |
michael@0 | 304 | try { |
michael@0 | 305 | pixelBuffer.wait(); |
michael@0 | 306 | } catch (InterruptedException ie) { |
michael@0 | 307 | } |
michael@0 | 308 | mPixelBuffer = null; |
michael@0 | 309 | } |
michael@0 | 310 | return pixelBuffer; |
michael@0 | 311 | } |
michael@0 | 312 | |
michael@0 | 313 | private RenderContext createScreenContext(ImmutableViewportMetrics metrics, PointF offset) { |
michael@0 | 314 | RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight()); |
michael@0 | 315 | RectF pageRect = metrics.getPageRect(); |
michael@0 | 316 | |
michael@0 | 317 | return createContext(viewport, pageRect, 1.0f, offset); |
michael@0 | 318 | } |
michael@0 | 319 | |
michael@0 | 320 | private RenderContext createPageContext(ImmutableViewportMetrics metrics, PointF offset) { |
michael@0 | 321 | RectF viewport = metrics.getViewport(); |
michael@0 | 322 | RectF pageRect = metrics.getPageRect(); |
michael@0 | 323 | float zoomFactor = metrics.zoomFactor; |
michael@0 | 324 | |
michael@0 | 325 | return createContext(new RectF(RectUtils.round(viewport)), pageRect, zoomFactor, offset); |
michael@0 | 326 | } |
michael@0 | 327 | |
michael@0 | 328 | private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor, PointF offset) { |
michael@0 | 329 | if (mCoordBuffer == null) { |
michael@0 | 330 | throw new IllegalStateException(); |
michael@0 | 331 | } |
michael@0 | 332 | return new RenderContext(viewport, pageRect, zoomFactor, offset, mPositionHandle, mTextureHandle, |
michael@0 | 333 | mCoordBuffer); |
michael@0 | 334 | } |
michael@0 | 335 | |
michael@0 | 336 | private void updateDroppedFrames(long frameStartTime) { |
michael@0 | 337 | int frameElapsedTime = (int)((System.nanoTime() - frameStartTime) / NANOS_PER_MS); |
michael@0 | 338 | |
michael@0 | 339 | /* Update the running statistics. */ |
michael@0 | 340 | mFrameTimingsSum -= mFrameTimings[mCurrentFrame]; |
michael@0 | 341 | mFrameTimingsSum += frameElapsedTime; |
michael@0 | 342 | mDroppedFrames -= (mFrameTimings[mCurrentFrame] + 1) / MAX_FRAME_TIME; |
michael@0 | 343 | mDroppedFrames += (frameElapsedTime + 1) / MAX_FRAME_TIME; |
michael@0 | 344 | |
michael@0 | 345 | mFrameTimings[mCurrentFrame] = frameElapsedTime; |
michael@0 | 346 | mCurrentFrame = (mCurrentFrame + 1) % mFrameTimings.length; |
michael@0 | 347 | |
michael@0 | 348 | int averageTime = mFrameTimingsSum / mFrameTimings.length; |
michael@0 | 349 | mFrameRateLayer.beginTransaction(); // called on compositor thread |
michael@0 | 350 | try { |
michael@0 | 351 | mFrameRateLayer.setText(averageTime + " ms/" + mDroppedFrames); |
michael@0 | 352 | } finally { |
michael@0 | 353 | mFrameRateLayer.endTransaction(); |
michael@0 | 354 | } |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | /* Given the new dimensions for the surface, moves the frame rate layer appropriately. */ |
michael@0 | 358 | private void moveFrameRateLayer(int width, int height) { |
michael@0 | 359 | mFrameRateLayer.beginTransaction(); // called on compositor thread |
michael@0 | 360 | try { |
michael@0 | 361 | Rect position = new Rect(width - FRAME_RATE_METER_WIDTH - 8, |
michael@0 | 362 | height - FRAME_RATE_METER_HEIGHT + 8, |
michael@0 | 363 | width - 8, |
michael@0 | 364 | height + 8); |
michael@0 | 365 | mFrameRateLayer.setPosition(position); |
michael@0 | 366 | } finally { |
michael@0 | 367 | mFrameRateLayer.endTransaction(); |
michael@0 | 368 | } |
michael@0 | 369 | } |
michael@0 | 370 | |
michael@0 | 371 | void checkMonitoringEnabled() { |
michael@0 | 372 | /* Do this I/O off the main thread to minimize its impact on startup time. */ |
michael@0 | 373 | new Thread(new Runnable() { |
michael@0 | 374 | @Override |
michael@0 | 375 | public void run() { |
michael@0 | 376 | Context context = mView.getContext(); |
michael@0 | 377 | SharedPreferences preferences = context.getSharedPreferences("GeckoApp", 0); |
michael@0 | 378 | if (preferences.getBoolean("showFrameRate", false)) { |
michael@0 | 379 | IntSize frameRateLayerSize = new IntSize(FRAME_RATE_METER_WIDTH, FRAME_RATE_METER_HEIGHT); |
michael@0 | 380 | mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--"); |
michael@0 | 381 | moveFrameRateLayer(mView.getWidth(), mView.getHeight()); |
michael@0 | 382 | } |
michael@0 | 383 | mProfileRender = Log.isLoggable(PROFTAG, Log.DEBUG); |
michael@0 | 384 | } |
michael@0 | 385 | }).start(); |
michael@0 | 386 | } |
michael@0 | 387 | |
michael@0 | 388 | /* |
michael@0 | 389 | * create a vertex shader type (GLES20.GL_VERTEX_SHADER) |
michael@0 | 390 | * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) |
michael@0 | 391 | */ |
michael@0 | 392 | public static int loadShader(int type, String shaderCode) { |
michael@0 | 393 | int shader = GLES20.glCreateShader(type); |
michael@0 | 394 | GLES20.glShaderSource(shader, shaderCode); |
michael@0 | 395 | GLES20.glCompileShader(shader); |
michael@0 | 396 | return shader; |
michael@0 | 397 | } |
michael@0 | 398 | |
michael@0 | 399 | public Frame createFrame(ImmutableViewportMetrics metrics) { |
michael@0 | 400 | return new Frame(metrics); |
michael@0 | 401 | } |
michael@0 | 402 | |
michael@0 | 403 | class FadeRunnable implements Runnable { |
michael@0 | 404 | private boolean mStarted; |
michael@0 | 405 | private long mRunAt; |
michael@0 | 406 | |
michael@0 | 407 | void scheduleStartFade(long delay) { |
michael@0 | 408 | mRunAt = SystemClock.elapsedRealtime() + delay; |
michael@0 | 409 | if (!mStarted) { |
michael@0 | 410 | mView.postDelayed(this, delay); |
michael@0 | 411 | mStarted = true; |
michael@0 | 412 | } |
michael@0 | 413 | } |
michael@0 | 414 | |
michael@0 | 415 | void scheduleNextFadeFrame() { |
michael@0 | 416 | if (mStarted) { |
michael@0 | 417 | Log.e(LOGTAG, "scheduleNextFadeFrame() called while scheduled for starting fade"); |
michael@0 | 418 | } |
michael@0 | 419 | mView.postDelayed(this, 1000L / 60L); // request another frame at 60fps |
michael@0 | 420 | } |
michael@0 | 421 | |
michael@0 | 422 | boolean timeToFade() { |
michael@0 | 423 | return !mStarted; |
michael@0 | 424 | } |
michael@0 | 425 | |
michael@0 | 426 | @Override |
michael@0 | 427 | public void run() { |
michael@0 | 428 | long timeDelta = mRunAt - SystemClock.elapsedRealtime(); |
michael@0 | 429 | if (timeDelta > 0) { |
michael@0 | 430 | // the run-at time was pushed back, so reschedule |
michael@0 | 431 | mView.postDelayed(this, timeDelta); |
michael@0 | 432 | } else { |
michael@0 | 433 | // reached the run-at time, execute |
michael@0 | 434 | mStarted = false; |
michael@0 | 435 | mView.requestRender(); |
michael@0 | 436 | } |
michael@0 | 437 | } |
michael@0 | 438 | } |
michael@0 | 439 | |
michael@0 | 440 | public class Frame { |
michael@0 | 441 | // The timestamp recording the start of this frame. |
michael@0 | 442 | private long mFrameStartTime; |
michael@0 | 443 | // A fixed snapshot of the viewport metrics that this frame is using to render content. |
michael@0 | 444 | private ImmutableViewportMetrics mFrameMetrics; |
michael@0 | 445 | // A rendering context for page-positioned layers, and one for screen-positioned layers. |
michael@0 | 446 | private RenderContext mPageContext, mScreenContext; |
michael@0 | 447 | // Whether a layer was updated. |
michael@0 | 448 | private boolean mUpdated; |
michael@0 | 449 | private final Rect mPageRect; |
michael@0 | 450 | private final Rect mAbsolutePageRect; |
michael@0 | 451 | private final PointF mRenderOffset; |
michael@0 | 452 | |
michael@0 | 453 | public Frame(ImmutableViewportMetrics metrics) { |
michael@0 | 454 | mFrameMetrics = metrics; |
michael@0 | 455 | |
michael@0 | 456 | // Work out the offset due to margins |
michael@0 | 457 | Layer rootLayer = mView.getLayerClient().getRoot(); |
michael@0 | 458 | mRenderOffset = mFrameMetrics.getMarginOffset(); |
michael@0 | 459 | mPageContext = createPageContext(metrics, mRenderOffset); |
michael@0 | 460 | mScreenContext = createScreenContext(metrics, mRenderOffset); |
michael@0 | 461 | |
michael@0 | 462 | RectF pageRect = mFrameMetrics.getPageRect(); |
michael@0 | 463 | mAbsolutePageRect = RectUtils.round(pageRect); |
michael@0 | 464 | |
michael@0 | 465 | PointF origin = mFrameMetrics.getOrigin(); |
michael@0 | 466 | pageRect.offset(-origin.x, -origin.y); |
michael@0 | 467 | mPageRect = RectUtils.round(pageRect); |
michael@0 | 468 | } |
michael@0 | 469 | |
michael@0 | 470 | private void setScissorRect() { |
michael@0 | 471 | Rect scissorRect = transformToScissorRect(mPageRect); |
michael@0 | 472 | GLES20.glEnable(GLES20.GL_SCISSOR_TEST); |
michael@0 | 473 | GLES20.glScissor(scissorRect.left, scissorRect.top, |
michael@0 | 474 | scissorRect.width(), scissorRect.height()); |
michael@0 | 475 | } |
michael@0 | 476 | |
michael@0 | 477 | private Rect transformToScissorRect(Rect rect) { |
michael@0 | 478 | IntSize screenSize = new IntSize(mFrameMetrics.getSize()); |
michael@0 | 479 | |
michael@0 | 480 | int left = Math.max(0, rect.left); |
michael@0 | 481 | int top = Math.max(0, rect.top); |
michael@0 | 482 | int right = Math.min(screenSize.width, rect.right); |
michael@0 | 483 | int bottom = Math.min(screenSize.height, rect.bottom); |
michael@0 | 484 | |
michael@0 | 485 | Rect scissorRect = new Rect(left, screenSize.height - bottom, right, |
michael@0 | 486 | (screenSize.height - bottom) + (bottom - top)); |
michael@0 | 487 | scissorRect.offset(Math.round(-mRenderOffset.x), Math.round(-mRenderOffset.y)); |
michael@0 | 488 | |
michael@0 | 489 | return scissorRect; |
michael@0 | 490 | } |
michael@0 | 491 | |
michael@0 | 492 | /** This function is invoked via JNI; be careful when modifying signature. */ |
michael@0 | 493 | @JNITarget |
michael@0 | 494 | public void beginDrawing() { |
michael@0 | 495 | mFrameStartTime = System.nanoTime(); |
michael@0 | 496 | |
michael@0 | 497 | TextureReaper.get().reap(); |
michael@0 | 498 | TextureGenerator.get().fill(); |
michael@0 | 499 | |
michael@0 | 500 | mUpdated = true; |
michael@0 | 501 | |
michael@0 | 502 | Layer rootLayer = mView.getLayerClient().getRoot(); |
michael@0 | 503 | |
michael@0 | 504 | // Run through pre-render tasks |
michael@0 | 505 | runRenderTasks(mTasks, false, mFrameStartTime); |
michael@0 | 506 | |
michael@0 | 507 | if (!mPageContext.fuzzyEquals(mLastPageContext) && !mView.isFullScreen()) { |
michael@0 | 508 | // The viewport or page changed, so show the scrollbars again |
michael@0 | 509 | // as per UX decision. Don't do this if we're in full-screen mode though. |
michael@0 | 510 | mVertScrollLayer.unfade(); |
michael@0 | 511 | mHorizScrollLayer.unfade(); |
michael@0 | 512 | mFadeRunnable.scheduleStartFade(ScrollbarLayer.FADE_DELAY); |
michael@0 | 513 | } else if (mFadeRunnable.timeToFade()) { |
michael@0 | 514 | boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade(); |
michael@0 | 515 | if (stillFading) { |
michael@0 | 516 | mFadeRunnable.scheduleNextFadeFrame(); |
michael@0 | 517 | } |
michael@0 | 518 | } |
michael@0 | 519 | mLastPageContext = mPageContext; |
michael@0 | 520 | |
michael@0 | 521 | /* Update layers. */ |
michael@0 | 522 | if (rootLayer != null) { |
michael@0 | 523 | // Called on compositor thread. |
michael@0 | 524 | mUpdated &= rootLayer.update(mPageContext); |
michael@0 | 525 | } |
michael@0 | 526 | |
michael@0 | 527 | if (mFrameRateLayer != null) { |
michael@0 | 528 | // Called on compositor thread. |
michael@0 | 529 | mUpdated &= mFrameRateLayer.update(mScreenContext); |
michael@0 | 530 | } |
michael@0 | 531 | |
michael@0 | 532 | mUpdated &= mVertScrollLayer.update(mPageContext); // called on compositor thread |
michael@0 | 533 | mUpdated &= mHorizScrollLayer.update(mPageContext); // called on compositor thread |
michael@0 | 534 | |
michael@0 | 535 | for (Layer layer : mExtraLayers) { |
michael@0 | 536 | mUpdated &= layer.update(mPageContext); // called on compositor thread |
michael@0 | 537 | } |
michael@0 | 538 | } |
michael@0 | 539 | |
michael@0 | 540 | /** Retrieves the bounds for the layer, rounded in such a way that it |
michael@0 | 541 | * can be used as a mask for something that will render underneath it. |
michael@0 | 542 | * This will round the bounds inwards, but stretch the mask towards any |
michael@0 | 543 | * near page edge, where near is considered to be 'within 2 pixels'. |
michael@0 | 544 | * Returns null if the given layer is null. |
michael@0 | 545 | */ |
michael@0 | 546 | private Rect getMaskForLayer(Layer layer) { |
michael@0 | 547 | if (layer == null) { |
michael@0 | 548 | return null; |
michael@0 | 549 | } |
michael@0 | 550 | |
michael@0 | 551 | RectF bounds = RectUtils.contract(layer.getBounds(mPageContext), 1.0f, 1.0f); |
michael@0 | 552 | Rect mask = RectUtils.roundIn(bounds); |
michael@0 | 553 | |
michael@0 | 554 | // If the mask is within two pixels of any page edge, stretch it over |
michael@0 | 555 | // that edge. This is to avoid drawing thin slivers when masking |
michael@0 | 556 | // layers. |
michael@0 | 557 | if (mask.top <= 2) { |
michael@0 | 558 | mask.top = -1; |
michael@0 | 559 | } |
michael@0 | 560 | if (mask.left <= 2) { |
michael@0 | 561 | mask.left = -1; |
michael@0 | 562 | } |
michael@0 | 563 | |
michael@0 | 564 | // Because we're drawing relative to the page-rect, we only need to |
michael@0 | 565 | // take into account its width and height (and not its origin) |
michael@0 | 566 | int pageRight = mPageRect.width(); |
michael@0 | 567 | int pageBottom = mPageRect.height(); |
michael@0 | 568 | |
michael@0 | 569 | if (mask.right >= pageRight - 2) { |
michael@0 | 570 | mask.right = pageRight + 1; |
michael@0 | 571 | } |
michael@0 | 572 | if (mask.bottom >= pageBottom - 2) { |
michael@0 | 573 | mask.bottom = pageBottom + 1; |
michael@0 | 574 | } |
michael@0 | 575 | |
michael@0 | 576 | return mask; |
michael@0 | 577 | } |
michael@0 | 578 | |
michael@0 | 579 | private void clear(int color) { |
michael@0 | 580 | GLES20.glClearColor(((color >> 16) & 0xFF) / 255.0f, |
michael@0 | 581 | ((color >> 8) & 0xFF) / 255.0f, |
michael@0 | 582 | (color & 0xFF) / 255.0f, |
michael@0 | 583 | 0.0f); |
michael@0 | 584 | // The bits set here need to match up with those used |
michael@0 | 585 | // in gfx/layers/opengl/LayerManagerOGL.cpp. |
michael@0 | 586 | GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | |
michael@0 | 587 | GLES20.GL_DEPTH_BUFFER_BIT); |
michael@0 | 588 | } |
michael@0 | 589 | |
michael@0 | 590 | /** This function is invoked via JNI; be careful when modifying signature. */ |
michael@0 | 591 | @JNITarget |
michael@0 | 592 | public void drawBackground() { |
michael@0 | 593 | // Any GL state which is changed here must be restored in |
michael@0 | 594 | // CompositorOGL::RestoreState |
michael@0 | 595 | |
michael@0 | 596 | GLES20.glDisable(GLES20.GL_SCISSOR_TEST); |
michael@0 | 597 | |
michael@0 | 598 | // Draw the overscroll background area as a solid color |
michael@0 | 599 | clear(mOverscrollColor); |
michael@0 | 600 | |
michael@0 | 601 | // Update background color. |
michael@0 | 602 | mBackgroundColor = mView.getBackgroundColor(); |
michael@0 | 603 | |
michael@0 | 604 | // Clear the page area to the page background colour. |
michael@0 | 605 | setScissorRect(); |
michael@0 | 606 | clear(mBackgroundColor); |
michael@0 | 607 | GLES20.glDisable(GLES20.GL_SCISSOR_TEST); |
michael@0 | 608 | } |
michael@0 | 609 | |
michael@0 | 610 | // Draws the layer the client added to us. |
michael@0 | 611 | void drawRootLayer() { |
michael@0 | 612 | Layer rootLayer = mView.getLayerClient().getRoot(); |
michael@0 | 613 | if (rootLayer == null) { |
michael@0 | 614 | return; |
michael@0 | 615 | } |
michael@0 | 616 | |
michael@0 | 617 | rootLayer.draw(mPageContext); |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | @JNITarget |
michael@0 | 621 | public void drawForeground() { |
michael@0 | 622 | // Any GL state which is changed here must be restored in |
michael@0 | 623 | // CompositorOGL::RestoreState |
michael@0 | 624 | |
michael@0 | 625 | /* Draw any extra layers that were added (likely plugins) */ |
michael@0 | 626 | if (mExtraLayers.size() > 0) { |
michael@0 | 627 | for (Layer layer : mExtraLayers) { |
michael@0 | 628 | layer.draw(mPageContext); |
michael@0 | 629 | } |
michael@0 | 630 | } |
michael@0 | 631 | |
michael@0 | 632 | /* Draw the vertical scrollbar. */ |
michael@0 | 633 | if (mPageRect.height() > mFrameMetrics.getHeight()) |
michael@0 | 634 | mVertScrollLayer.draw(mPageContext); |
michael@0 | 635 | |
michael@0 | 636 | /* Draw the horizontal scrollbar. */ |
michael@0 | 637 | if (mPageRect.width() > mFrameMetrics.getWidth()) |
michael@0 | 638 | mHorizScrollLayer.draw(mPageContext); |
michael@0 | 639 | |
michael@0 | 640 | /* Measure how much of the screen is checkerboarding */ |
michael@0 | 641 | Layer rootLayer = mView.getLayerClient().getRoot(); |
michael@0 | 642 | if ((rootLayer != null) && |
michael@0 | 643 | (mProfileRender || PanningPerfAPI.isRecordingCheckerboard())) { |
michael@0 | 644 | // Calculate the incompletely rendered area of the page |
michael@0 | 645 | float checkerboard = 1.0f - GeckoAppShell.computeRenderIntegrity(); |
michael@0 | 646 | |
michael@0 | 647 | PanningPerfAPI.recordCheckerboard(checkerboard); |
michael@0 | 648 | if (checkerboard < 0.0f || checkerboard > 1.0f) { |
michael@0 | 649 | Log.e(LOGTAG, "Checkerboard value out of bounds: " + checkerboard); |
michael@0 | 650 | } |
michael@0 | 651 | |
michael@0 | 652 | mCompleteFramesRendered += 1.0f - checkerboard; |
michael@0 | 653 | mFramesRendered ++; |
michael@0 | 654 | |
michael@0 | 655 | if (mFrameStartTime - mProfileOutputTime > NANOS_PER_SECOND) { |
michael@0 | 656 | mProfileOutputTime = mFrameStartTime; |
michael@0 | 657 | printCheckerboardStats(); |
michael@0 | 658 | } |
michael@0 | 659 | } |
michael@0 | 660 | |
michael@0 | 661 | runRenderTasks(mTasks, true, mFrameStartTime); |
michael@0 | 662 | |
michael@0 | 663 | /* Draw the FPS. */ |
michael@0 | 664 | if (mFrameRateLayer != null) { |
michael@0 | 665 | updateDroppedFrames(mFrameStartTime); |
michael@0 | 666 | |
michael@0 | 667 | GLES20.glEnable(GLES20.GL_BLEND); |
michael@0 | 668 | GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); |
michael@0 | 669 | mFrameRateLayer.draw(mScreenContext); |
michael@0 | 670 | } |
michael@0 | 671 | } |
michael@0 | 672 | |
michael@0 | 673 | /** This function is invoked via JNI; be careful when modifying signature. */ |
michael@0 | 674 | @JNITarget |
michael@0 | 675 | public void endDrawing() { |
michael@0 | 676 | // If a layer update requires further work, schedule another redraw |
michael@0 | 677 | if (!mUpdated) |
michael@0 | 678 | mView.requestRender(); |
michael@0 | 679 | |
michael@0 | 680 | PanningPerfAPI.recordFrameTime(); |
michael@0 | 681 | |
michael@0 | 682 | /* Used by robocop for testing purposes */ |
michael@0 | 683 | IntBuffer pixelBuffer = mPixelBuffer; |
michael@0 | 684 | if (mUpdated && pixelBuffer != null) { |
michael@0 | 685 | synchronized (pixelBuffer) { |
michael@0 | 686 | pixelBuffer.position(0); |
michael@0 | 687 | GLES20.glReadPixels(0, 0, (int)mScreenContext.viewport.width(), |
michael@0 | 688 | (int)mScreenContext.viewport.height(), GLES20.GL_RGBA, |
michael@0 | 689 | GLES20.GL_UNSIGNED_BYTE, pixelBuffer); |
michael@0 | 690 | pixelBuffer.notify(); |
michael@0 | 691 | } |
michael@0 | 692 | } |
michael@0 | 693 | |
michael@0 | 694 | // Remove background color once we've painted. GeckoLayerClient is |
michael@0 | 695 | // responsible for setting this flag before current document is |
michael@0 | 696 | // composited. |
michael@0 | 697 | if (mView.getPaintState() == LayerView.PAINT_BEFORE_FIRST) { |
michael@0 | 698 | mView.post(new Runnable() { |
michael@0 | 699 | @Override |
michael@0 | 700 | public void run() { |
michael@0 | 701 | mView.getChildAt(0).setBackgroundColor(Color.TRANSPARENT); |
michael@0 | 702 | } |
michael@0 | 703 | }); |
michael@0 | 704 | mView.setPaintState(LayerView.PAINT_AFTER_FIRST); |
michael@0 | 705 | } |
michael@0 | 706 | mLastFrameTime = mFrameStartTime; |
michael@0 | 707 | } |
michael@0 | 708 | } |
michael@0 | 709 | |
michael@0 | 710 | @Override |
michael@0 | 711 | public void onTabChanged(final Tab tab, Tabs.TabEvents msg, Object data) { |
michael@0 | 712 | // Sets the background of the newly selected tab. This background color |
michael@0 | 713 | // gets cleared in endDrawing(). This function runs on the UI thread, |
michael@0 | 714 | // but other code that touches the paint state is run on the compositor |
michael@0 | 715 | // thread, so this may need to be changed if any problems appear. |
michael@0 | 716 | if (msg == Tabs.TabEvents.SELECTED) { |
michael@0 | 717 | if (mView != null) { |
michael@0 | 718 | if (mView.getChildAt(0) != null) { |
michael@0 | 719 | mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor()); |
michael@0 | 720 | } |
michael@0 | 721 | mView.setPaintState(LayerView.PAINT_START); |
michael@0 | 722 | } |
michael@0 | 723 | } |
michael@0 | 724 | } |
michael@0 | 725 | } |