Bug 709120 - Update non-visible tiles asynchronously. r=pcwalton
authorChris Lord <chrislord.net@gmail.com>
Fri, 06 Jan 2012 11:22:52 +0000
changeset 85153 a1f1e4d685c0008b2cb6a4038a9004ef0ab6b598
parent 85152 32d494dce3427db139995626c5d4aa6e94ced784
child 85154 441a93f42c6ded5d33f32852c9e70fd98134b716
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspcwalton
bugs709120
milestone12.0a1
Bug 709120 - Update non-visible tiles asynchronously. r=pcwalton Only update tiles that intersect with the screen, and asynchronously update invisible tiles over multiple subsequent frames. This has the effect of spreading out non-critical texture-upload over time and removes some juddering on devices that don't support gralloc.
mobile/android/base/gfx/Layer.java
mobile/android/base/gfx/LayerRenderer.java
mobile/android/base/gfx/MultiTileLayer.java
mobile/android/base/gfx/TileLayer.java
mobile/android/base/gfx/WidgetTileLayer.java
--- a/mobile/android/base/gfx/Layer.java
+++ b/mobile/android/base/gfx/Layer.java
@@ -56,30 +56,35 @@ public abstract class Layer {
     private LayerView mView;
 
     public Layer() {
         mTransactionLock = new ReentrantLock();
         mOrigin = new Point(0, 0);
         mResolution = 1.0f;
     }
 
-    /** Updates the layer. */
-    public final void update(GL10 gl) {
+    /**
+     * Updates the layer. This returns false if there is still work to be done
+     * after this update.
+     */
+    public final boolean update(GL10 gl, RenderContext context) {
         if (mTransactionLock.isHeldByCurrentThread()) {
             throw new RuntimeException("draw() called while transaction lock held by this " +
                                        "thread?!");
         }
 
         if (mTransactionLock.tryLock()) {
             try {
-                performUpdates(gl);
+                return performUpdates(gl, context);
             } finally {
                 mTransactionLock.unlock();
             }
         }
+
+        return false;
     }
 
     /** Subclasses override this function to draw the layer. */
     public abstract void draw(RenderContext context);
 
     /** Subclasses override this function to provide access to the size of the layer. */
     public abstract IntSize getSize();
 
@@ -153,27 +158,30 @@ public abstract class Layer {
         if (!mInTransaction)
             throw new RuntimeException("setResolution() is only valid inside a transaction");
         mNewResolution = newResolution;
     }
 
     /**
      * Subclasses may override this method to perform custom layer updates. This will be called
      * with the transaction lock held. Subclass implementations of this method must call the
-     * superclass implementation.
+     * superclass implementation. Returns false if there is still work to be done after this
+     * update is complete.
      */
-    protected void performUpdates(GL10 gl) {
+    protected boolean performUpdates(GL10 gl, RenderContext context) {
         if (mNewOrigin != null) {
             mOrigin = mNewOrigin;
             mNewOrigin = null;
         }
         if (mNewResolution != 0.0f) {
             mResolution = mNewResolution;
             mNewResolution = 0.0f;
         }
+
+        return true;
     }
 
     public static class RenderContext {
         public final RectF viewport;
         public final FloatSize pageSize;
         public final float zoomFactor;
 
         public RenderContext(RectF aViewport, FloatSize aPageSize, float aZoomFactor) {
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -142,16 +142,18 @@ public class LayerRenderer implements GL
     public void onDrawFrame(GL10 gl) {
         long frameStartTime = SystemClock.uptimeMillis();
 
         TextureReaper.get().reap(gl);
 
         LayerController controller = mView.getController();
         RenderContext screenContext = createScreenContext();
 
+        boolean updated = true;
+
         synchronized (controller) {
             Layer rootLayer = controller.getRoot();
             RenderContext pageContext = createPageContext();
 
             if (!pageContext.fuzzyEquals(mLastPageContext)) {
                 // the viewport or page changed, so show the scrollbars again
                 // as per UX decision
                 mVertScrollLayer.unfade();
@@ -161,22 +163,22 @@ public class LayerRenderer implements GL
                 boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade();
                 if (stillFading) {
                     mFadeRunnable.scheduleNextFadeFrame();
                 }
             }
             mLastPageContext = pageContext;
 
             /* Update layers. */
-            if (rootLayer != null) rootLayer.update(gl);
-            mShadowLayer.update(gl);
-            mCheckerboardLayer.update(gl);
-            mFrameRateLayer.update(gl);
-            mVertScrollLayer.update(gl);
-            mHorizScrollLayer.update(gl);
+            if (rootLayer != null) updated &= rootLayer.update(gl, pageContext);
+            updated &= mShadowLayer.update(gl, pageContext);
+            updated &= mCheckerboardLayer.update(gl, screenContext);
+            updated &= mFrameRateLayer.update(gl, screenContext);
+            updated &= mVertScrollLayer.update(gl, pageContext);
+            updated &= mHorizScrollLayer.update(gl, pageContext);
 
             /* Draw the background. */
             gl.glClearColor(BACKGROUND_COLOR_R, BACKGROUND_COLOR_G, BACKGROUND_COLOR_B, 1.0f);
             gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 
             /* Draw the drop shadow, if we need to. */
             Rect pageRect = getPageRect();
             RectF untransformedPageRect = new RectF(0.0f, 0.0f, pageRect.width(),
@@ -215,16 +217,20 @@ public class LayerRenderer implements GL
                 gl.glEnable(GL10.GL_BLEND);
                 gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
                 mFrameRateLayer.draw(screenContext);
             } finally {
                 gl.glDisable(GL10.GL_BLEND);
             }
         }
 
+        // If a layer update requires further work, schedule another redraw
+        if (!updated)
+            mView.requestRender();
+
         PanningPerfAPI.recordFrameTime();
     }
 
     private RenderContext createScreenContext() {
         LayerController layerController = mView.getController();
         IntSize viewportSize = new IntSize(layerController.getViewportSize());
         RectF viewport = new RectF(0.0f, 0.0f, viewportSize.width, viewportSize.height);
         FloatSize pageSize = new FloatSize(layerController.getPageSize());
--- a/mobile/android/base/gfx/MultiTileLayer.java
+++ b/mobile/android/base/gfx/MultiTileLayer.java
@@ -37,16 +37,17 @@
 
 package org.mozilla.gecko.gfx;
 
 import org.mozilla.gecko.gfx.CairoImage;
 import org.mozilla.gecko.gfx.IntSize;
 import org.mozilla.gecko.gfx.SingleTileLayer;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.util.Log;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import javax.microedition.khronos.opengles.GL10;
 
 /**
  * Encapsulates the logic needed to draw a layer made of multiple tiles.
  *
@@ -154,23 +155,69 @@ public class MultiTileLayer extends Laye
 
         // Set tile origins and resolution
         refreshTileMetrics(getOrigin(), getResolution(), false);
 
         mBufferSize = size;
     }
 
     @Override
-    protected void performUpdates(GL10 gl) {
-        super.performUpdates(gl);
+    protected boolean performUpdates(GL10 gl, RenderContext context) {
+        super.performUpdates(gl, context);
 
         validateTiles();
 
-        for (SingleTileLayer layer : mTiles)
-            layer.performUpdates(gl);
+        // Iterate over the tiles and decide which ones we'll be drawing
+        int dirtyTiles = 0;
+        boolean screenUpdateDone = false;
+        SingleTileLayer firstDirtyTile = null;
+        for (SingleTileLayer layer : mTiles) {
+            // First do a non-texture update to make sure coordinates are
+            // up-to-date.
+            boolean invalid = layer.getSkipTextureUpdate();
+            layer.setSkipTextureUpdate(true);
+            layer.performUpdates(gl, context);
+
+            RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize()));
+            boolean isDirty = layer.isDirty();
+
+            if (isDirty) {
+                if (!RectF.intersects(layerBounds, context.viewport)) {
+                    if (firstDirtyTile == null)
+                        firstDirtyTile = layer;
+                    dirtyTiles ++;
+                    invalid = true;
+                } else {
+                    // This tile intersects with the screen and is dirty,
+                    // update it immediately.
+                    layer.setSkipTextureUpdate(false);
+                    screenUpdateDone = true;
+                    layer.performUpdates(gl, context);
+                    invalid = false;
+                }
+            }
+
+            // We use the SkipTextureUpdate flag as a marker of a tile's
+            // validity. This is required, as sometimes layers are drawn
+            // without updating first, and we mustn't draw tiles that have
+            // been marked as invalid that we haven't updated.
+            layer.setSkipTextureUpdate(invalid);
+        }
+
+        // Now if no tiles that intersect with the screen were updated, update
+        // a single tile that doesn't (if there are any). This has the effect
+        // of spreading out non-critical texture upload over time, and smoothing
+        // upload-related hitches.
+        if (!screenUpdateDone && firstDirtyTile != null) {
+            firstDirtyTile.setSkipTextureUpdate(false);
+            firstDirtyTile.performUpdates(gl, context);
+            dirtyTiles --;
+        }
+
+        return (dirtyTiles == 0);
     }
 
     private void refreshTileMetrics(Point origin, float resolution, boolean inTransaction) {
         int x = 0, y = 0;
         IntSize size = getSize();
         for (SingleTileLayer layer : mTiles) {
             if (!inTransaction)
                 layer.beginTransaction(null);
@@ -216,13 +263,22 @@ public class MultiTileLayer extends Laye
         for (SingleTileLayer layer : mTiles)
             layer.endTransaction();
 
         super.endTransaction();
     }
 
     @Override
     public void draw(RenderContext context) {
-        for (SingleTileLayer layer : mTiles)
-            layer.draw(context);
+        for (SingleTileLayer layer : mTiles) {
+            // We use the SkipTextureUpdate flag as a validity flag. If it's false,
+            // the contents of this tile are invalid and we shouldn't draw it.
+            if (layer.getSkipTextureUpdate())
+                continue;
+
+            // Avoid work, only draw tiles that intersect with the viewport
+            RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize()));
+            if (RectF.intersects(layerBounds, context.viewport))
+                layer.draw(context);
+        }
     }
 }
 
--- a/mobile/android/base/gfx/TileLayer.java
+++ b/mobile/android/base/gfx/TileLayer.java
@@ -54,22 +54,24 @@ import java.nio.FloatBuffer;
  */
 public abstract class TileLayer extends Layer {
     private static final String LOGTAG = "GeckoTileLayer";
 
     private final Rect mDirtyRect;
     private final CairoImage mImage;
     private final boolean mRepeat;
     private IntSize mSize;
+    private boolean mSkipTextureUpdate;
     private int[] mTextureIDs;
 
     public TileLayer(boolean repeat, CairoImage image) {
         mRepeat = repeat;
         mImage = image;
         mSize = new IntSize(0, 0);
+        mSkipTextureUpdate = false;
 
         IntSize bufferSize = mImage.getSize();
         mDirtyRect = new Rect();
     }
 
     @Override
     public IntSize getSize() { return mImage.getSize(); }
 
@@ -93,16 +95,20 @@ public abstract class TileLayer extends 
         mDirtyRect.union(rect);
     }
 
     public void invalidate() {
         IntSize bufferSize = mImage.getSize();
         invalidate(new Rect(0, 0, bufferSize.width, bufferSize.height));
     }
 
+    public boolean isDirty() {
+        return mImage.getSize().isPositive() && (mTextureIDs == null || !mDirtyRect.isEmpty());
+    }
+
     private void validateTexture(GL10 gl) {
         /* Calculate the ideal texture size. This must be a power of two if
          * the texture is repeated or OpenGL ES 2.0 isn't supported, as
          * OpenGL ES 2.0 is required for NPOT texture support (without
          * extensions), but doesn't support repeating NPOT textures.
          *
          * XXX Currently, we don't pick a GLES 2.0 context, so always round.
          */
@@ -121,34 +127,48 @@ public abstract class TileLayer extends 
 
                 // Free the texture immediately, so we don't incur a
                 // temporarily increased memory usage.
                 TextureReaper.get().reap(gl);
             }
         }
     }
 
+    /** Tells the tile not to update the texture on the next update. */
+    public void setSkipTextureUpdate(boolean skip) {
+        mSkipTextureUpdate = skip;
+    }
+
+    public boolean getSkipTextureUpdate() {
+        return mSkipTextureUpdate;
+    }
+
     @Override
-    protected void performUpdates(GL10 gl) {
-        super.performUpdates(gl);
+    protected boolean performUpdates(GL10 gl, RenderContext context) {
+        super.performUpdates(gl, context);
+
+        if (mSkipTextureUpdate)
+            return false;
 
         // Reallocate the texture if the size has changed
         validateTexture(gl);
 
         // Don't do any work if the image has an invalid size.
         if (!mImage.getSize().isPositive())
-            return;
+            return true;
 
         // If we haven't allocated a texture, assume the whole region is dirty
         if (mTextureIDs == null)
             uploadFullTexture(gl);
         else
             uploadDirtyRect(gl, mDirtyRect);
 
         mDirtyRect.setEmpty();
+
+        return true;
     }
 
     private void uploadFullTexture(GL10 gl) {
         IntSize bufferSize = mImage.getSize();
         uploadDirtyRect(gl, new Rect(0, 0, bufferSize.width, bufferSize.height));
     }
 
     private void uploadDirtyRect(GL10 gl, Rect dirtyRect) {
--- a/mobile/android/base/gfx/WidgetTileLayer.java
+++ b/mobile/android/base/gfx/WidgetTileLayer.java
@@ -72,26 +72,28 @@ public class WidgetTileLayer extends Lay
 
     @Override
     protected void finalize() throws Throwable {
         if (mTextureIDs != null)
             TextureReaper.get().add(mTextureIDs);
     }
 
     @Override
-    protected void performUpdates(GL10 gl) {
-        super.performUpdates(gl);
+    protected boolean performUpdates(GL10 gl, RenderContext context) {
+        super.performUpdates(gl, context);
 
         if (mTextureIDs == null) {
             mTextureIDs = new int[1];
             GLES11.glGenTextures(1, mTextureIDs, 0);
         }
 
         bindAndSetGLParameters();
         GeckoAppShell.bindWidgetTexture();
+
+        return true;
     }
 
     @Override
     public void draw(RenderContext context) {
         // mTextureIDs may be null here during startup if Layer.java's draw method
         // failed to acquire the transaction lock and call performUpdates.
         if (!initialized())
             return;