Backout 91eebc6bdb59 & 7d776b291014 (bug 717283) for Android native reftest crashes
authorEd Morley <bmo@edmorley.co.uk>
Sun, 22 Jan 2012 11:55:18 +0000
changeset 86299 3ff0e8b46d702cd6f35ba5b7a349ed54cba5b2dc
parent 86298 71370660e59d3342c2328bac06d21ae5c6561707
child 86300 d7f703ae1c8c7014be939f31244ddde5422f6691
child 86301 ca7d87ab38b6e7ef55edd6ac2f300b8c1fcbfc01
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)
bugs717283
milestone12.0a1
backs out91eebc6bdb597e482f7d522c06dec7ceeb051fc1
Backout 91eebc6bdb59 & 7d776b291014 (bug 717283) for Android native reftest crashes
mobile/android/base/gfx/GeckoSoftwareLayerClient.java
mobile/android/base/gfx/Layer.java
mobile/android/base/gfx/LayerRenderer.java
mobile/android/base/gfx/MultiTileLayer.java
mobile/android/base/gfx/RectUtils.java
mobile/android/base/gfx/SingleTileLayer.java
mobile/android/base/gfx/TileLayer.java
mobile/android/chrome/content/browser.js
widget/android/AndroidJavaWrappers.cpp
widget/android/AndroidJavaWrappers.h
widget/android/nsWindow.cpp
--- a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java
+++ b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java
@@ -75,25 +75,19 @@ public class GeckoSoftwareLayerClient ex
 
     private Context mContext;
     private int mFormat;
     private IntSize mScreenSize, mViewportSize;
     private IntSize mBufferSize;
     private ByteBuffer mBuffer;
     private Layer mTileLayer;
 
-    /* The viewport that Gecko is currently displaying. */
+    /* The viewport rect that Gecko is currently displaying. */
     private ViewportMetrics mGeckoViewport;
 
-    /* The viewport that Gecko will display when drawing is finished */
-    private ViewportMetrics mNewGeckoViewport;
-
-    /* The offset used to make sure tiles are snapped to the pixel grid */
-    private Point mRenderOffset;
-
     private CairoImage mCairoImage;
 
     private static final IntSize TILE_SIZE = new IntSize(256, 256);
 
     private static final long MIN_VIEWPORT_CHANGE_DELAY = 350L;
     private long mLastViewportChangeTime;
     private boolean mPendingViewportAdjust;
     private boolean mViewportSizeChanged;
@@ -109,26 +103,27 @@ public class GeckoSoftwareLayerClient ex
     private boolean mUpdateViewportOnEndDraw;
 
     public GeckoSoftwareLayerClient(Context context) {
         mContext = context;
 
         mScreenSize = new IntSize(0, 0);
         mBufferSize = new IntSize(0, 0);
         mFormat = CairoImage.FORMAT_RGB16_565;
-        mRenderOffset = new Point(0, 0);
 
         mCairoImage = new CairoImage() {
             @Override
             public ByteBuffer getBuffer() { return mBuffer; }
             @Override
             public IntSize getSize() { return mBufferSize; }
             @Override
             public int getFormat() { return mFormat; }
         };
+
+        mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE);
     }
 
     public int getWidth() {
         return mBufferSize.width;
     }
 
     public int getHeight() {
         return mBufferSize.height;
@@ -151,167 +146,124 @@ public class GeckoSoftwareLayerClient ex
 
         layerController.setRoot(mTileLayer);
         if (mGeckoViewport != null) {
             layerController.setViewportMetrics(mGeckoViewport);
         }
 
         GeckoAppShell.registerGeckoEventListener("Viewport:UpdateAndDraw", this);
         GeckoAppShell.registerGeckoEventListener("Viewport:UpdateLater", this);
-        GeckoAppShell.registerGeckoEventListener("Document:Shown", this);
-        GeckoAppShell.registerGeckoEventListener("Tab:Selected", this);
 
         // This needs to happen before a call to sendResizeEventIfNecessary
         // happens, but only needs to be called once. As that is only called by
         // the layer controller or this, here is a safe place to do so.
         if (mTileLayer instanceof MultiTileLayer) {
             GeckoEvent event = new GeckoEvent(GeckoEvent.TILE_SIZE, TILE_SIZE);
             GeckoAppShell.sendEventToGecko(event);
         }
 
         sendResizeEventIfNecessary();
     }
 
-    private boolean setHasDirectTexture(boolean hasDirectTexture) {
-        if (mTileLayer != null && hasDirectTexture == mHasDirectTexture)
-            return false;
+    private void setHasDirectTexture(boolean hasDirectTexture) {
+        if (hasDirectTexture == mHasDirectTexture)
+            return;
 
         mHasDirectTexture = hasDirectTexture;
 
         IntSize tileSize;
         if (mHasDirectTexture) {
             mTileLayer = new WidgetTileLayer(mCairoImage);
             tileSize = new IntSize(0, 0);
-            mRenderOffset.set(0, 0);
         } else {
             mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE);
             tileSize = TILE_SIZE;
         }
 
         getLayerController().setRoot(mTileLayer);
 
         GeckoEvent event = new GeckoEvent(GeckoEvent.TILE_SIZE, tileSize);
         GeckoAppShell.sendEventToGecko(event);
 
         // Force a resize event to be sent because the results of this
         // are different depending on what tile system we're using
         sendResizeEventIfNecessary(true);
-
-        return true;
     }
 
-    public boolean beginDrawing(int width, int height, String metadata, boolean hasDirectTexture) {
-        // If we've changed surface types, cancel this draw
-        if (setHasDirectTexture(hasDirectTexture))
-            return false;
-
+    public void beginDrawing(int width, int height) {
         beginTransaction(mTileLayer);
 
-        try {
-            JSONObject viewportObject = new JSONObject(metadata);
-            mNewGeckoViewport = new ViewportMetrics(viewportObject);
-
-            // We only need to set a render offset/allocate buffer memory if
-            // we're using MultiTileLayer. Otherwise, just synchronise the
-            // buffer size and return.
-            if (!(mTileLayer instanceof MultiTileLayer)) {
-                if (mBufferSize.width != width || mBufferSize.height != height)
-                    mBufferSize = new IntSize(width, height);
-                return true;
-            }
-
-            // If the origin has changed, alter the rendering offset so that
-            // rendering is snapped to the tile grid and clear the invalid area.
-            boolean originChanged = true;
-            Point origin = PointUtils.round(mNewGeckoViewport.getDisplayportOrigin());
-
-            if (mGeckoViewport != null) {
-                Point oldOrigin = PointUtils.round(mGeckoViewport.getDisplayportOrigin());
-                originChanged = !origin.equals(oldOrigin);
-            }
-
-            if (originChanged) {
-                Point tileOrigin = new Point((origin.x / TILE_SIZE.width) * TILE_SIZE.width,
-                                             (origin.y / TILE_SIZE.height) * TILE_SIZE.height);
-                mRenderOffset.set(origin.x - tileOrigin.x, origin.y - tileOrigin.y);
-                ((MultiTileLayer)mTileLayer).invalidateBuffer();
-            }
-        } catch (JSONException e) {
-            Log.e(LOGTAG, "Bad viewport description: " + metadata);
-            throw new RuntimeException(e);
-        }
-
         if (mBufferSize.width != width || mBufferSize.height != height) {
             mBufferSize = new IntSize(width, height);
 
-            // We over-allocate to allow for the render offset. nsWindow
-            // assumes that this will happen.
-            IntSize realBufferSize = new IntSize(width + TILE_SIZE.width,
-                                                 height + TILE_SIZE.height);
+            // Reallocate the buffer if necessary
 
-            // Reallocate the buffer if necessary
-            int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;
-            int size = realBufferSize.getArea() * bpp;
+            // * 2 because it's a 16-bit buffer (so 2 bytes per pixel).
+            int size = mBufferSize.getArea() * 2;
             if (mBuffer == null || mBuffer.capacity() != size) {
                 // Free the old buffer
                 if (mBuffer != null) {
                     GeckoAppShell.freeDirectBuffer(mBuffer);
                     mBuffer = null;
                 }
 
                 mBuffer = GeckoAppShell.allocateDirectBuffer(size);
             }
         }
-
-        return true;
     }
 
-    private void updateViewport(final boolean onlyUpdatePageSize) {
-        // save and restore the viewport size stored in java; never let the
-        // JS-side viewport dimensions override the java-side ones because
-        // java is the One True Source of this information, and allowing JS
-        // to override can lead to race conditions where this data gets clobbered.
-        FloatSize viewportSize = getLayerController().getViewportSize();
-        mGeckoViewport = mNewGeckoViewport;
-        mGeckoViewport.setSize(viewportSize);
+    private void updateViewport(String viewportDescription, final boolean onlyUpdatePageSize) {
+        try {
+            JSONObject viewportObject = new JSONObject(viewportDescription);
+
+            // save and restore the viewport size stored in java; never let the
+            // JS-side viewport dimensions override the java-side ones because
+            // java is the One True Source of this information, and allowing JS
+            // to override can lead to race conditions where this data gets clobbered.
+            FloatSize viewportSize = getLayerController().getViewportSize();
+            mGeckoViewport = new ViewportMetrics(viewportObject);
+            mGeckoViewport.setSize(viewportSize);
 
-        LayerController controller = getLayerController();
-        PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin();
-        Point tileOrigin = PointUtils.round(displayportOrigin);
-        tileOrigin.offset(-mRenderOffset.x, -mRenderOffset.y);
-        mTileLayer.setOrigin(tileOrigin);
-        mTileLayer.setResolution(mGeckoViewport.getZoomFactor());
+            LayerController controller = getLayerController();
+            PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin();
+            mTileLayer.setOrigin(PointUtils.round(displayportOrigin));
+            mTileLayer.setResolution(mGeckoViewport.getZoomFactor());
 
-        if (onlyUpdatePageSize) {
-            // Don't adjust page size when zooming unless zoom levels are
-            // approximately equal.
-            if (FloatUtils.fuzzyEquals(controller.getZoomFactor(),
-                    mGeckoViewport.getZoomFactor()))
-                controller.setPageSize(mGeckoViewport.getPageSize());
-        } else {
-            controller.setViewportMetrics(mGeckoViewport);
-            controller.abortPanZoomAnimation();
+            if (onlyUpdatePageSize) {
+                // Don't adjust page size when zooming unless zoom levels are
+                // approximately equal.
+                if (FloatUtils.fuzzyEquals(controller.getZoomFactor(),
+                        mGeckoViewport.getZoomFactor()))
+                    controller.setPageSize(mGeckoViewport.getPageSize());
+            } else {
+                Log.d(LOGTAG, "Received viewport update from gecko");
+                controller.setViewportMetrics(mGeckoViewport);
+                controller.abortPanZoomAnimation();
+            }
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Bad viewport description: " + viewportDescription);
+            throw new RuntimeException(e);
         }
     }
 
     /*
      * TODO: Would be cleaner if this took an android.graphics.Rect instead, but that would require
      * a little more JNI magic.
      */
-    public void endDrawing(int x, int y, int width, int height) {
+    public void endDrawing(int x, int y, int width, int height, String metadata, boolean hasDirectTexture) {
         synchronized (getLayerController()) {
             try {
-                updateViewport(!mUpdateViewportOnEndDraw);
+                updateViewport(metadata, !mUpdateViewportOnEndDraw);
                 mUpdateViewportOnEndDraw = false;
+                Rect rect = new Rect(x, y, x + width, y + height);
 
-                if (mTileLayer instanceof MultiTileLayer) {
-                    Rect rect = new Rect(x, y, x + width, y + height);
-                    rect.offset(mRenderOffset.x, mRenderOffset.y);
+                setHasDirectTexture(hasDirectTexture);
+
+                if (!mHasDirectTexture)
                     ((MultiTileLayer)mTileLayer).invalidate(rect);
-                }
             } finally {
                 endTransaction(mTileLayer);
             }
         }
     }
 
     public ViewportMetrics getGeckoViewportMetrics() {
         // Return a copy, as we modify this inside the Gecko thread
@@ -320,51 +272,56 @@ public class GeckoSoftwareLayerClient ex
         return null;
     }
 
     public void copyPixelsFromMultiTileLayer(Bitmap target) {
         Canvas c = new Canvas(target);
         ByteBuffer tileBuffer = mBuffer.slice();
         int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;
 
-        for (int y = 0; y <= mBufferSize.height; y += TILE_SIZE.height) {
-            for (int x = 0; x <= mBufferSize.width; x += TILE_SIZE.width) {
+        for (int y = 0; y < mBufferSize.height; y += TILE_SIZE.height) {
+            for (int x = 0; x < mBufferSize.width; x += TILE_SIZE.width) {
+                // Calculate tile size
+                IntSize tileSize = new IntSize(Math.min(mBufferSize.width - x, TILE_SIZE.width),
+                                               Math.min(mBufferSize.height - y, TILE_SIZE.height));
+
                 // Create a Bitmap from this tile
-                Bitmap tile = Bitmap.createBitmap(TILE_SIZE.width, TILE_SIZE.height,
+                Bitmap tile = Bitmap.createBitmap(tileSize.width, tileSize.height,
                                                   CairoUtils.cairoFormatTobitmapConfig(mFormat));
                 tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer());
 
                 // Copy the tile to the master Bitmap and recycle it
-                c.drawBitmap(tile, x - mRenderOffset.x, y - mRenderOffset.y, null);
+                c.drawBitmap(tile, x, y, null);
                 tile.recycle();
 
                 // Progress the buffer to the next tile
-                tileBuffer.position(TILE_SIZE.getArea() * bpp);
+                tileBuffer.position(tileSize.getArea() * bpp);
                 tileBuffer = tileBuffer.slice();
             }
         }
     }
 
     public Bitmap getBitmap() {
-        if (mTileLayer == null)
-            return null;
-
         // Begin a tile transaction, otherwise the buffer can be destroyed while
         // we're reading from it.
         beginTransaction(mTileLayer);
         try {
             if (mBuffer == null || mBufferSize.width <= 0 || mBufferSize.height <= 0)
                 return null;
             try {
                 Bitmap b = null;
 
                 if (mTileLayer instanceof MultiTileLayer) {
                     b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height,
-                                            CairoUtils.cairoFormatTobitmapConfig(mFormat));
+                                               CairoUtils.cairoFormatTobitmapConfig(mFormat));
                     copyPixelsFromMultiTileLayer(b);
+                } else if (mTileLayer instanceof SingleTileLayer) {
+                    b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height,
+                                               CairoUtils.cairoFormatTobitmapConfig(mFormat));
+                    b.copyPixelsFromBuffer(mBuffer.asIntBuffer());
                 } else {
                     Log.w(LOGTAG, "getBitmap() called on a layer (" + mTileLayer + ") we don't know how to get a bitmap from");
                 }
 
                 return b;
             } catch (OutOfMemoryError oom) {
                 Log.w(LOGTAG, "Unable to create bitmap", oom);
                 return null;
@@ -374,20 +331,16 @@ public class GeckoSoftwareLayerClient ex
         }
     }
 
     /** Returns the back buffer. This function is for Gecko to use. */
     public ByteBuffer lockBuffer() {
         return mBuffer;
     }
 
-    public Point getRenderOffset() {
-        return mRenderOffset;
-    }
-
     /**
      * Gecko calls this function to signal that it is done with the back buffer. After this call,
      * it is forbidden for Gecko to touch the buffer.
      */
     public void unlockBuffer() {
         /* no-op */
     }
 
@@ -412,28 +365,28 @@ public class GeckoSoftwareLayerClient ex
             return;
         }
 
         mScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels);
         IntSize bufferSize;
 
         // Round up depending on layer implementation to remove texture wastage
         if (mTileLayer instanceof MultiTileLayer) {
-            // Round to the next multiple of the tile size
+            // Round to the next multiple of the tile size, respecting maximum texture size
             bufferSize = new IntSize(((mScreenSize.width + LayerController.MIN_BUFFER.width - 1) / TILE_SIZE.width + 1) * TILE_SIZE.width,
                                      ((mScreenSize.height + LayerController.MIN_BUFFER.height - 1) / TILE_SIZE.height + 1) * TILE_SIZE.height);
 
         } else {
             int maxSize = getLayerController().getView().getMaxTextureSize();
 
             // XXX Integrate gralloc/tiling work to circumvent this
             if (mScreenSize.width > maxSize || mScreenSize.height > maxSize)
                 throw new RuntimeException("Screen size of " + mScreenSize + " larger than maximum texture size of " + maxSize);
 
-            // Round to next power of two until we have NPOT texture support, respecting maximum texture size
+            // Round to next power of two until we have NPOT texture support
             bufferSize = new IntSize(Math.min(maxSize, IntSize.nextPowerOfTwo(mScreenSize.width + LayerController.MIN_BUFFER.width)),
                                      Math.min(maxSize, IntSize.nextPowerOfTwo(mScreenSize.height + LayerController.MIN_BUFFER.height)));
         }
 
         Log.i(LOGTAG, "Screen-size changed to " + mScreenSize);
         GeckoEvent event = new GeckoEvent(GeckoEvent.SIZE_CHANGED,
                                           bufferSize.width, bufferSize.height,
                                           metrics.widthPixels, metrics.heightPixels);
@@ -494,22 +447,12 @@ public class GeckoSoftwareLayerClient ex
         if ("Viewport:UpdateAndDraw".equals(event)) {
             mUpdateViewportOnEndDraw = true;
 
             // Redraw everything.
             Rect rect = new Rect(0, 0, mBufferSize.width, mBufferSize.height);
             GeckoAppShell.sendEventToGecko(new GeckoEvent(GeckoEvent.DRAW, rect));
         } else if ("Viewport:UpdateLater".equals(event)) {
             mUpdateViewportOnEndDraw = true;
-        } else if (("Document:Shown".equals(event) ||
-                    "Tab:Selected".equals(event)) &&
-                   (mTileLayer instanceof MultiTileLayer)) {
-            beginTransaction(mTileLayer);
-            try {
-                ((MultiTileLayer)mTileLayer).invalidateTiles();
-                ((MultiTileLayer)mTileLayer).invalidateBuffer();
-            } finally {
-                endTransaction(mTileLayer);
-            }
         }
     }
 }
 
--- a/mobile/android/base/gfx/Layer.java
+++ b/mobile/android/base/gfx/Layer.java
@@ -58,41 +58,33 @@ public abstract class Layer {
     public Layer() {
         mTransactionLock = new ReentrantLock();
         mOrigin = new Point(0, 0);
         mResolution = 1.0f;
     }
 
     /**
      * Updates the layer. This returns false if there is still work to be done
-     * after this update. If the layer is not already in a transaction, the
-     * lock will be acquired and a transaction will automatically begin and
-     * end around the update.
+     * after this update.
      */
     public final boolean update(GL10 gl, RenderContext context) {
-        boolean startTransaction = true;
         if (mTransactionLock.isHeldByCurrentThread()) {
-            startTransaction = false;
+            throw new RuntimeException("draw() called while transaction lock held by this " +
+                                       "thread?!");
         }
 
-        // If we're not already in a transaction and we can't acquire the lock,
-        // bail out.
-        if (startTransaction && !mTransactionLock.tryLock()) {
-            return false;
-        }
-
-        mInTransaction = true;
-        try {
-            return performUpdates(gl, context);
-        } finally {
-            if (startTransaction) {
-                mInTransaction = false;
+        if (mTransactionLock.tryLock()) {
+            try {
+                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();
 
@@ -112,16 +104,17 @@ public abstract class Layer {
      * This function may block, so you should never call this on the main UI thread.
      */
     public void beginTransaction(LayerView aView) {
         if (mTransactionLock.isHeldByCurrentThread())
             throw new RuntimeException("Nested transactions are not supported");
         mTransactionLock.lock();
         mView = aView;
         mInTransaction = true;
+        mNewResolution = mResolution;
     }
 
     public void beginTransaction() {
         beginTransaction(null);
     }
 
     /** Call this when you're done modifying the layer. */
     public void endTransaction() {
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -96,40 +96,22 @@ public class LayerRenderer implements GL
 
     public LayerRenderer(LayerView view) {
         mView = view;
 
         LayerController controller = view.getController();
 
         CairoImage backgroundImage = new BufferedCairoImage(controller.getBackgroundPattern());
         mBackgroundLayer = new SingleTileLayer(true, backgroundImage);
-        mBackgroundLayer.beginTransaction(null);
-        try {
-            mBackgroundLayer.invalidate();
-        } finally {
-            mBackgroundLayer.endTransaction();
-        }
 
         CairoImage checkerboardImage = new BufferedCairoImage(controller.getCheckerboardPattern());
         mCheckerboardLayer = new SingleTileLayer(true, checkerboardImage);
-        mCheckerboardLayer.beginTransaction(null);
-        try {
-            mCheckerboardLayer.invalidate();
-        } finally {
-            mCheckerboardLayer.endTransaction();
-        }
 
         CairoImage shadowImage = new BufferedCairoImage(controller.getShadowPattern());
         mShadowLayer = new NinePatchTileLayer(shadowImage);
-        mShadowLayer.beginTransaction(null);
-        try {
-            mShadowLayer.invalidate();
-        } finally {
-            mShadowLayer.endTransaction();
-        }
 
         IntSize frameRateLayerSize = new IntSize(FRAME_RATE_METER_WIDTH, FRAME_RATE_METER_HEIGHT);
         mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--");
 
         mHorizScrollLayer = ScrollbarLayer.create(false);
         mVertScrollLayer = ScrollbarLayer.create(true);
         mFadeRunnable = new FadeRunnable();
 
--- a/mobile/android/base/gfx/MultiTileLayer.java
+++ b/mobile/android/base/gfx/MultiTileLayer.java
@@ -32,454 +32,253 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.gfx;
 
-import org.mozilla.gecko.FloatUtils;
 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.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.Region;
 import android.util.Log;
-import java.lang.Long;
 import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.ListIterator;
+import java.util.ArrayList;
 import javax.microedition.khronos.opengles.GL10;
 
 /**
  * Encapsulates the logic needed to draw a layer made of multiple tiles.
  *
  * TODO: Support repeating.
  */
 public class MultiTileLayer extends Layer {
     private static final String LOGTAG = "GeckoMultiTileLayer";
 
     private final CairoImage mImage;
-    private final IntSize mTileSize;
+    private IntSize mTileSize;
     private IntSize mBufferSize;
-    private Region mDirtyRegion;
-    private Region mValidRegion;
-    private final LinkedList<SubTile> mTiles;
-    private final HashMap<Long, SubTile> mPositionHash;
+    private final ArrayList<SingleTileLayer> mTiles;
 
     public MultiTileLayer(CairoImage image, IntSize tileSize) {
         super();
 
         mImage = image;
         mTileSize = tileSize;
         mBufferSize = new IntSize(0, 0);
-        mDirtyRegion = new Region();
-        mValidRegion = new Region();
-        mTiles = new LinkedList<SubTile>();
-        mPositionHash = new HashMap<Long, SubTile>();
+        mTiles = new ArrayList<SingleTileLayer>();
     }
 
-    /**
-     * Invalidates a sub-region of the layer. Data will be uploaded from the
-     * backing buffer over subsequent calls to update().
-     * This method is only valid inside a transaction.
-     */
     public void invalidate(Rect dirtyRect) {
-        if (!inTransaction()) {
+        if (!inTransaction())
             throw new RuntimeException("invalidate() is only valid inside a transaction");
+
+        int x = 0, y = 0;
+        IntSize size = getSize();
+        for (SingleTileLayer layer : mTiles) {
+            Rect tileRect = new Rect(x, y, x + mTileSize.width, y + mTileSize.height);
+
+            if (tileRect.intersect(dirtyRect)) {
+                tileRect.offset(-x, -y);
+                layer.invalidate(tileRect);
+            }
+
+            x += mTileSize.width;
+            if (x >= size.width) {
+                x = 0;
+                y += mTileSize.height;
+            }
         }
-
-        mDirtyRegion.union(dirtyRect);
-        mValidRegion.union(dirtyRect);
     }
 
-    /**
-     * Invalidates the backing buffer. Data will not be uploaded from an invalid
-     * backing buffer. This method is only valid inside a transaction.
-     */
-    public void invalidateBuffer() {
-        if (!inTransaction()) {
-            throw new RuntimeException("invalidateBuffer() is only valid inside a transaction");
-        }
-
-        mDirtyRegion.setEmpty();
-        mValidRegion.setEmpty();
+    public void invalidate() {
+        for (SingleTileLayer layer : mTiles)
+            layer.invalidate();
     }
 
     @Override
     public IntSize getSize() {
         return mImage.getSize();
     }
 
-    /**
-     * Makes sure there are enough tiles to accommodate the buffer image.
-     */
     private void validateTiles() {
         IntSize size = getSize();
 
         if (size.equals(mBufferSize))
             return;
 
-        mBufferSize = size;
-
-        // Shrink/grow tile pool
-        int nTiles = (Math.round(size.width / (float)mTileSize.width) + 1) *
-                     (Math.round(size.height / (float)mTileSize.height) + 1);
-        if (mTiles.size() < nTiles) {
-            Log.i(LOGTAG, "Tile pool growing from " + mTiles.size() + " to " + nTiles);
+        // Regenerate tiles
+        mTiles.clear();
+        int offset = 0;
+        final int format = mImage.getFormat();
+        final ByteBuffer buffer = mImage.getBuffer().slice();
+        final int bpp = CairoUtils.bitsPerPixelForCairoFormat(format) / 8;
+        for (int y = 0; y < size.height; y += mTileSize.height) {
+            for (int x = 0; x < size.width; x += mTileSize.width) {
+                // Create a CairoImage implementation that returns a
+                // tile from the parent CairoImage. It's assumed that
+                // the tiles are stored in series.
+                final IntSize layerSize =
+                    new IntSize(Math.min(mTileSize.width, size.width - x),
+                                Math.min(mTileSize.height, size.height - y));
+                final int tileOffset = offset;
 
-            for (int i = 0; i < nTiles; i++) {
-                mTiles.add(new SubTile(new SubImage(mImage, mTileSize)));
-            }
-        } else if (mTiles.size() > nTiles) {
-            Log.i(LOGTAG, "Tile pool shrinking from " + mTiles.size() + " to " + nTiles);
+                CairoImage subImage = new CairoImage() {
+                    @Override
+                    public ByteBuffer getBuffer() {
+                        // Create a ByteBuffer that shares the data of the original
+                        // buffer, but is positioned and limited so that only the
+                        // tile data is accessible.
+                        buffer.position(tileOffset);
+                        ByteBuffer tileBuffer = buffer.slice();
+                        tileBuffer.limit(layerSize.getArea() * bpp);
+
+                        return tileBuffer;
+                    }
 
-            // Remove tiles from the beginning of the list, as these are
-            // least recently used tiles
-            for (int i = mTiles.size(); i > nTiles; i--) {
-                SubTile tile = mTiles.get(0);
-                if (tile.key != null) {
-                    mPositionHash.remove(tile.key);
-                }
-                mTiles.remove(0);
+                    @Override
+                    public IntSize getSize() {
+                        return layerSize;
+                    }
+
+                    @Override
+                    public int getFormat() {
+                        return format;
+                    }
+                };
+
+                mTiles.add(new SingleTileLayer(subImage));
+                offset += layerSize.getArea() * bpp;
             }
         }
 
-        // A buffer size probably means a layout change, so invalidate all tiles.
-        invalidateTiles();
-    }
-
-    /**
-     * Returns a Long representing the given Point. Used for hashing.
-     */
-    private Long longFromPoint(Point point) {
-        // Assign 32 bits for each dimension of the point.
-        return new Long((((long)point.x) << 32) | point.y);
-    }
-
-    /**
-     * Performs the necessary functions to update the specified properties of
-     * a sub-tile.
-     */
-    private void updateTile(GL10 gl, RenderContext context, SubTile tile, Point tileOrigin, Rect dirtyRect, boolean reused) {
-        tile.beginTransaction(null);
-        try {
-            if (reused) {
-                // Invalidate any area that isn't represented in the current
-                // buffer. This is done as SingleTileLayer always updates the
-                // entire width, regardless of the dirty-rect's width, and so
-                // can override existing data.
-                Point origin = getOrigin();
-                Rect validRect = tile.getValidTextureArea();
-                validRect.offset(tileOrigin.x - origin.x, tileOrigin.y - origin.y);
-                Region validRegion = new Region(validRect);
-                validRegion.op(mValidRegion, Region.Op.INTERSECT);
+        // Set tile origins and resolution
+        refreshTileMetrics(getOrigin(), getResolution(), false);
 
-                // SingleTileLayer can't draw complex regions, so in that case,
-                // just invalidate the entire area.
-                tile.invalidateTexture();
-                if (!validRegion.isComplex()) {
-                    validRect.set(validRegion.getBounds());
-                    validRect.offset(origin.x - tileOrigin.x, origin.y - tileOrigin.y);
-                }
-            } else {
-                // Update tile metrics
-                tile.setOrigin(tileOrigin);
-                tile.setResolution(getResolution());
-
-                // Make sure that non-reused tiles are marked as invalid before
-                // uploading new content.
-                tile.invalidateTexture();
-
-                // (Re)Place in the position hash for quick retrieval.
-                if (tile.key != null) {
-                    mPositionHash.remove(tile.key);
-                }
-                tile.key = longFromPoint(tileOrigin);
-                mPositionHash.put(tile.key, tile);
-            }
-
-            // Invalidate the area we want to upload.
-            tile.invalidate(dirtyRect);
-
-            // Perform updates and mark texture as valid.
-            if (!tile.performUpdates(gl, context)) {
-                Log.e(LOGTAG, "Sub-tile failed to update fully");
-            }
-        } finally {
-            tile.endTransaction();
-        }
+        mBufferSize = size;
     }
 
     @Override
     protected boolean performUpdates(GL10 gl, RenderContext context) {
         super.performUpdates(gl, context);
 
         validateTiles();
 
-        // Bail out early if we have nothing to do.
-        if (mDirtyRegion.isEmpty() || mTiles.isEmpty()) {
-            return true;
-        }
-
-        // Check that we're capable of updating from this origin.
-        Point origin = getOrigin();
-        if ((origin.x % mTileSize.width) != 0 || (origin.y % mTileSize.height) != 0) {
-            Log.e(LOGTAG, "MultiTileLayer doesn't support non tile-aligned origins! (" +
-                  origin.x + ", " + origin.y + ")");
-            return true;
-        }
+        // 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);
 
-        // Transform the viewport into tile-space so we can see what part of the
-        // dirty region intersects with it.
-        // We update any tiles intersecting with the screen before tiles
-        // intersecting with the viewport.
-        // XXX Maybe we want to to split this update even further to update
-        //     checkerboard area before updating screen regions with old data.
-        //     Note that this could provide inconsistent views, so we may not
-        //     want to do this.
-        Rect tilespaceViewport;
-        float scaleFactor = getResolution() / context.zoomFactor;
-        tilespaceViewport = RectUtils.roundOut(RectUtils.scale(context.viewport, scaleFactor));
-        tilespaceViewport.offset(-origin.x, -origin.y);
+            RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize()));
+            boolean isDirty = layer.isDirty();
 
-        // Expand tile-space viewport to tile boundaries
-        tilespaceViewport.left = (tilespaceViewport.left / mTileSize.width) * mTileSize.width;
-        tilespaceViewport.right += mTileSize.width - 1;
-        tilespaceViewport.right = (tilespaceViewport.right / mTileSize.width) * mTileSize.width;
-        tilespaceViewport.top = (tilespaceViewport.top / mTileSize.height) * mTileSize.height;
-        tilespaceViewport.bottom += mTileSize.height - 1;
-        tilespaceViewport.bottom = (tilespaceViewport.bottom / mTileSize.height) * mTileSize.height;
-
-        // Declare a region for storing the results of Region operations
-        Region opRegion = new Region();
-
-        // Test if the dirty region intersects with the screen
-        boolean updateVisible = false;
-        Region updateRegion = mDirtyRegion;
-        if (opRegion.op(tilespaceViewport, mDirtyRegion, Region.Op.INTERSECT)) {
-            updateVisible = true;
-            updateRegion = new Region(opRegion);
-        }
-
-        // Invalidate any tiles that are due to be replaced if their resolution
-        // doesn't match the parent layer resolution, and any tiles that are
-        // off-screen and off-buffer, as we cannot guarantee their validity.
-        //
-        // Note that we also cannot guarantee the validity of on-screen,
-        // off-buffer tiles, but this is a rare case that we allow for
-        // optimisation purposes.
-        //
-        // XXX Ideally, we want to remove this second invalidation clause
-        //     somehow. It may be possible to know if off-screen tiles are
-        //     valid by monitoring reflows on the browser element, or
-        //     something along these lines.
-        LinkedList<SubTile> invalidTiles = new LinkedList<SubTile>();
-        Rect bounds = mValidRegion.getBounds();
-        for (ListIterator<SubTile> i = mTiles.listIterator(); i.hasNext();) {
-            SubTile tile = i.next();
-
-            if (tile.key == null) {
-                continue;
+            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;
+                }
             }
 
-            RectF tileBounds = tile.getBounds(context, new FloatSize(tile.getSize()));
-            Rect tilespaceTileBounds =
-                RectUtils.round(RectUtils.scale(tileBounds, scaleFactor));
-            tilespaceTileBounds.offset(-origin.x, -origin.y);
+            // 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);
+        }
 
-            // First bracketed clause: Invalidate off-screen, off-buffer tiles
-            // Second: Invalidate visible tiles at the wrong resolution that have updates
-            if ((!Rect.intersects(bounds, tilespaceTileBounds) &&
-                 !Rect.intersects(tilespaceViewport, tilespaceTileBounds)) ||
-                (!FloatUtils.fuzzyEquals(tile.getResolution(), getResolution()) &&
-                 opRegion.op(tilespaceTileBounds, updateRegion, Region.Op.INTERSECT))) {
-                tile.invalidateTexture();
-
-                // Add to the list of invalid tiles and remove from the main list
-                invalidTiles.add(tile);
-                i.remove();
-
-                // Remove from the position hash
-                mPositionHash.remove(tile.key);
-                tile.key = null;
-            }
+        // 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 --;
         }
 
-        // Push invalid tiles to the head of the queue so they get used first
-        mTiles.addAll(0, invalidTiles);
+        return (dirtyTiles == 0);
+    }
 
-        // Update tiles
-        // Note, it's <= as the buffer is over-allocated due to render-offsetting.
-        for (int y = origin.y; y <= origin.y + mBufferSize.height; y += mTileSize.height) {
-            for (int x = origin.x; x <= origin.x + mBufferSize.width; x += mTileSize.width) {
-                // Does this tile intersect with the dirty region?
-                Rect tilespaceTileRect = new Rect(x - origin.x, y - origin.y,
-                                                  (x - origin.x) + mTileSize.width,
-                                                  (y - origin.y) + mTileSize.height);
-                if (!opRegion.op(tilespaceTileRect, updateRegion, Region.Op.INTERSECT)) {
-                    continue;
-                }
-
-                // Dirty tile, find out if we already have this tile to reuse.
-                boolean reusedTile = true;
-                Point tileOrigin = new Point(x, y);
-                SubTile tile = mPositionHash.get(longFromPoint(tileOrigin));
+    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);
 
-                // If we don't, get an unused tile (we store these at the head of the list).
-                if (tile == null) {
-                    tile = mTiles.removeFirst();
-                    reusedTile = false;
-                } else {
-                    mTiles.remove(tile);
-                }
-
-                // Place tile at the end of the tile-list so it isn't re-used.
-                mTiles.add(tile);
+            if (origin != null)
+                layer.setOrigin(new Point(origin.x + x, origin.y + y));
+            if (resolution >= 0.0f)
+                layer.setResolution(resolution);
 
-                // Work out the tile's invalid area in this tile's space.
-                if (opRegion.isComplex()) {
-                    Log.w(LOGTAG, "MultiTileLayer encountered complex dirty region");
-                }
-                Rect dirtyRect = opRegion.getBounds();
-                dirtyRect.offset(origin.x - x, origin.y - y);
+            if (!inTransaction)
+                layer.endTransaction();
 
-                // Update tile metrics and texture data
-                tile.x = (x - origin.x) / mTileSize.width;
-                tile.y = (y - origin.y) / mTileSize.height;
-                updateTile(gl, context, tile, tileOrigin, dirtyRect, reusedTile);
-
-                // If this update isn't visible, we only want to update one
-                // tile at a time.
-                if (!updateVisible) {
-                    mDirtyRegion.op(opRegion, Region.Op.XOR);
-                    return mDirtyRegion.isEmpty();
-                }
+            x += mTileSize.width;
+            if (x >= size.width) {
+                x = 0;
+                y += mTileSize.height;
             }
         }
+    }
 
-        // Remove the update region from the dirty region
-        mDirtyRegion.op(updateRegion, Region.Op.XOR);
+    @Override
+    public void setOrigin(Point newOrigin) {
+        super.setOrigin(newOrigin);
+        refreshTileMetrics(newOrigin, -1, true);
+    }
 
-        return mDirtyRegion.isEmpty();
+    @Override
+    public void setResolution(float newResolution) {
+        super.setResolution(newResolution);
+        refreshTileMetrics(null, newResolution, true);
     }
 
     @Override
     public void beginTransaction(LayerView aView) {
         super.beginTransaction(aView);
 
-        for (SubTile layer : mTiles) {
+        for (SingleTileLayer layer : mTiles)
             layer.beginTransaction(aView);
-        }
     }
 
     @Override
     public void endTransaction() {
-        for (SubTile layer : mTiles) {
+        for (SingleTileLayer layer : mTiles)
             layer.endTransaction();
-        }
 
         super.endTransaction();
     }
 
     @Override
     public void draw(RenderContext context) {
-        for (SubTile layer : mTiles) {
+        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);
         }
     }
-
-    /**
-     * Invalidates all sub-tiles. This should be called if the source backing
-     * this layer has changed. This method is only valid inside a transaction.
-     */
-    public void invalidateTiles() {
-        if (!inTransaction()) {
-            throw new RuntimeException("invalidateTiles() is only valid inside a transaction");
-        }
-
-        for (SubTile tile : mTiles) {
-            // Remove tile from position hash and mark it as invalid
-            if (tile.key != null) {
-                mPositionHash.remove(tile.key);
-                tile.key = null;
-            }
-            tile.invalidateTexture();
-        }
-    }
-
-    /**
-     * A SingleTileLayer extension with fields for relevant tile data that
-     * MultiTileLayer requires.
-     */
-    private static class SubTile extends SingleTileLayer {
-        public int x;
-        public int y;
-
-        public Long key;
-
-        public SubTile(SubImage aImage) {
-            super(aImage);
-
-            aImage.tile = this;
-        }
-    }
-
-    /**
-     * A CairoImage implementation that returns a tile from a parent CairoImage.
-     * This assumes that the parent image has a size that is a multiple of the
-     * tile size.
-     */
-    private static class SubImage extends CairoImage {
-        public SubTile tile;
-
-        private IntSize mTileSize;
-        private CairoImage mImage;
-
-        public SubImage(CairoImage image, IntSize tileSize) {
-            mTileSize = tileSize;
-            mImage = image;
-        }
-
-        @Override
-        public ByteBuffer getBuffer() {
-            // Create a ByteBuffer that shares the data of the original
-            // buffer, but is positioned and limited so that only the
-            // tile data is accessible.
-            IntSize bufferSize = mImage.getSize();
-            int bpp = CairoUtils.bitsPerPixelForCairoFormat(getFormat()) / 8;
-            int index = (tile.y * (bufferSize.width / mTileSize.width + 1)) + tile.x;
-
-            ByteBuffer buffer = mImage.getBuffer().slice();
-
-            try {
-                buffer.position(index * mTileSize.getArea() * bpp);
-                buffer = buffer.slice();
-                buffer.limit(mTileSize.getArea() * bpp);
-            } catch (IllegalArgumentException e) {
-                Log.e(LOGTAG, "Tile image-data out of bounds! Tile: (" +
-                      tile.x + ", " + tile.y + "), image (" + bufferSize + ")");
-                return null;
-            }
-
-            return buffer;
-        }
-
-        @Override
-        public IntSize getSize() {
-            return mTileSize;
-        }
-
-        @Override
-        public int getFormat() {
-            return mImage.getFormat();
-        }
-    }
 }
 
--- a/mobile/android/base/gfx/RectUtils.java
+++ b/mobile/android/base/gfx/RectUtils.java
@@ -94,28 +94,21 @@ public final class RectUtils {
     public static RectF scale(RectF rect, float scale) {
         float x = rect.left * scale;
         float y = rect.top * scale;
         return new RectF(x, y,
                          x + (rect.width() * scale),
                          y + (rect.height() * scale));
     }
 
-    /** Returns the nearest integer rect of the given rect. */
     public static Rect round(RectF rect) {
         return new Rect(Math.round(rect.left), Math.round(rect.top),
                         Math.round(rect.right), Math.round(rect.bottom));
     }
 
-    /** Returns the smallest integer rect that encapsulates the given rect. */
-    public static Rect roundOut(RectF rect) {
-        return new Rect((int)Math.floor(rect.left), (int)Math.floor(rect.top),
-                        (int)Math.ceil(rect.right), (int)Math.ceil(rect.bottom));
-    }
-
     public static IntSize getSize(Rect rect) {
         return new IntSize(rect.width(), rect.height());
     }
 
     /* Returns a new RectF which restricts a source rect to the area inside a second destination rect.
      * If the source rect is wider/taller than the destination rect, it's width/height will be shortened
      * (and its aspect ratio will NOT be maintained).
     */
--- a/mobile/android/base/gfx/SingleTileLayer.java
+++ b/mobile/android/base/gfx/SingleTileLayer.java
@@ -38,81 +38,59 @@
 package org.mozilla.gecko.gfx;
 
 import org.mozilla.gecko.gfx.CairoImage;
 import org.mozilla.gecko.gfx.CairoUtils;
 import org.mozilla.gecko.gfx.IntSize;
 import org.mozilla.gecko.gfx.LayerController;
 import org.mozilla.gecko.gfx.TileLayer;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.opengl.GLES11;
 import android.opengl.GLES11Ext;
 import android.util.Log;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.FloatBuffer;
 import javax.microedition.khronos.opengles.GL10;
 
 /**
  * Encapsulates the logic needed to draw a single textured tile.
  *
  * TODO: Repeating textures really should be their own type of layer.
  */
 public class SingleTileLayer extends TileLayer {
-    private static final String LOGTAG = "GeckoSingleTileLayer";
-
     public SingleTileLayer(CairoImage image) { this(false, image); }
 
     public SingleTileLayer(boolean repeat, CairoImage image) {
         super(repeat, image);
     }
 
     @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;
 
-        // If the texture contents is entirely invalid, we have nothing to draw.
-        Rect validTexture = getValidTextureArea();
-        if (validTexture.isEmpty())
-            return;
-
         GLES11.glBindTexture(GL10.GL_TEXTURE_2D, getTextureID());
 
         RectF bounds;
         int[] cropRect;
         IntSize size = getSize();
         RectF viewport = context.viewport;
 
         if (repeats()) {
-            if (!validTexture.equals(new Rect(0, 0, size.width, size.height))) {
-                Log.e(LOGTAG, "Drawing partial repeating textures is unsupported!");
-            }
-
             bounds = new RectF(0.0f, 0.0f, viewport.width(), viewport.height());
             int width = Math.round(viewport.width());
             int height = Math.round(-viewport.height());
             cropRect = new int[] { 0, size.height, width, height };
         } else {
             bounds = getBounds(context, new FloatSize(size));
-
-            float scaleFactor = bounds.width() / (float)size.width;
-            bounds.left += validTexture.left * scaleFactor;
-            bounds.top += validTexture.top * scaleFactor;
-            bounds.right -= (size.width - validTexture.right) * scaleFactor;
-            bounds.bottom -= (size.height - validTexture.bottom) * scaleFactor;
-
-            cropRect = new int[] { validTexture.left,
-                                   validTexture.bottom,
-                                   validTexture.width(),
-                                   -validTexture.height() };
+            cropRect = new int[] { 0, size.height, size.width, -size.height };
         }
 
         GLES11.glTexParameteriv(GL10.GL_TEXTURE_2D, GLES11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect,
                                 0);
 
         float height = bounds.height();
         float left = bounds.left - viewport.left;
         float top = viewport.height() - (bounds.top + height - viewport.top);
--- a/mobile/android/base/gfx/TileLayer.java
+++ b/mobile/android/base/gfx/TileLayer.java
@@ -54,24 +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 Rect mValidTextureRect;
+    private boolean mSkipTextureUpdate;
     private int[] mTextureIDs;
 
     public TileLayer(boolean repeat, CairoImage image) {
         mRepeat = repeat;
         mImage = image;
         mSize = new IntSize(0, 0);
-        mValidTextureRect = new Rect();
+        mSkipTextureUpdate = false;
 
         IntSize bufferSize = mImage.getSize();
         mDirtyRect = new Rect();
     }
 
     @Override
     public IntSize getSize() { return mImage.getSize(); }
 
@@ -127,117 +127,118 @@ 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 that its texture contents are invalid. This will also
-     * clear any invalidated area.
-     */
-    public void invalidateTexture() {
-        mValidTextureRect.setEmpty();
-        mDirtyRect.setEmpty();
+    /** Tells the tile not to update the texture on the next update. */
+    public void setSkipTextureUpdate(boolean skip) {
+        mSkipTextureUpdate = skip;
     }
 
-    /**
-     * Returns a handle to the valid texture area rectangle. Modifying this
-     * Rect will modify the valid texture area for this layer.
-     */
-    public Rect getValidTextureArea() {
-        return mValidTextureRect;
+    public boolean getSkipTextureUpdate() {
+        return mSkipTextureUpdate;
     }
 
     @Override
     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 true;
 
-        // Update the dirty region
-        uploadDirtyRect(gl, mDirtyRect);
+        // 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 uploadDirtyRect(GL10 gl, Rect dirtyRect) {
+    private void uploadFullTexture(GL10 gl) {
         IntSize bufferSize = mImage.getSize();
-        Rect bufferRect = new Rect(0, 0, bufferSize.width, bufferSize.height);
+        uploadDirtyRect(gl, new Rect(0, 0, bufferSize.width, bufferSize.height));
+    }
 
-        // Make sure the dirty region intersects with the buffer
-        dirtyRect.intersect(bufferRect);
-
+    private void uploadDirtyRect(GL10 gl, Rect dirtyRect) {
         // If we have nothing to upload, just return for now
         if (dirtyRect.isEmpty())
             return;
 
         // It's possible that the buffer will be null, check for that and return
         ByteBuffer imageBuffer = mImage.getBuffer();
         if (imageBuffer == null)
             return;
 
-        // Mark the dirty region as valid. Note, we assume that the valid area
-        // can be enclosed by a rectangle (ideally we'd use a Region, but it'd
-        // be slower and it probably isn't necessary).
-        mValidTextureRect.union(dirtyRect);
-
         boolean newlyCreated = false;
 
         if (mTextureIDs == null) {
             mTextureIDs = new int[1];
             gl.glGenTextures(mTextureIDs.length, mTextureIDs, 0);
             newlyCreated = true;
         }
 
+        IntSize bufferSize = mImage.getSize();
+        Rect bufferRect = new Rect(0, 0, bufferSize.width, bufferSize.height);
+
         int cairoFormat = mImage.getFormat();
         CairoGLInfo glInfo = new CairoGLInfo(cairoFormat);
 
         bindAndSetGLParameters(gl);
 
-        if (newlyCreated || dirtyRect.equals(bufferRect)) {
+        if (newlyCreated || dirtyRect.contains(bufferRect)) {
             if (mSize.equals(bufferSize)) {
                 gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, mSize.height,
                                 0, glInfo.format, glInfo.type, imageBuffer);
                 return;
             } else {
                 gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, mSize.height,
                                 0, glInfo.format, glInfo.type, null);
                 gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, bufferSize.width, bufferSize.height,
                                    glInfo.format, glInfo.type, imageBuffer);
                 return;
             }
         }
 
+        // Make sure that the dirty region intersects with the buffer rect,
+        // otherwise we'll end up with an invalid buffer pointer.
+        if (!Rect.intersects(dirtyRect, bufferRect))
+            return;
+
         /*
          * Upload the changed rect. We have to widen to the full width of the texture
          * because we can't count on the device having support for GL_EXT_unpack_subimage,
          * and going line-by-line is too slow.
          *
          * XXX We should still use GL_EXT_unpack_subimage when available.
          */
         Buffer viewBuffer = imageBuffer.slice();
         int bpp = CairoUtils.bitsPerPixelForCairoFormat(cairoFormat) / 8;
         int position = dirtyRect.top * bufferSize.width * bpp;
         if (position > viewBuffer.limit()) {
             Log.e(LOGTAG, "### Position outside tile! " + dirtyRect.top);
             return;
         }
 
         viewBuffer.position(position);
-        gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top,
-                           bufferSize.width, dirtyRect.height(),
+        gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top, bufferSize.width,
+                           Math.min(bufferSize.height - dirtyRect.top, dirtyRect.height()),
                            glInfo.format, glInfo.type, viewBuffer);
     }
 
     private void bindAndSetGLParameters(GL10 gl) {
         gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[0]);
         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
 
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2018,17 +2018,16 @@ Tab.prototype = {
   },
 
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "document-shown":
         // Is it on the top level?
         let contentDocument = aSubject;
         if (contentDocument == this.browser.contentDocument) {
-          sendMessageToJava({ gecko: { type: "Document:Shown" } });
           ViewportHandler.updateMetadata(this);
           this.documentIdForCurrentViewport = ViewportHandler.getIdForDocument(contentDocument);
         }
         break;
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([
--- a/widget/android/AndroidJavaWrappers.cpp
+++ b/widget/android/AndroidJavaWrappers.cpp
@@ -102,17 +102,16 @@ jmethodID AndroidAddress::jGetPremisesMe
 jmethodID AndroidAddress::jGetSubAdminAreaMethod;
 jmethodID AndroidAddress::jGetSubLocalityMethod;
 jmethodID AndroidAddress::jGetSubThoroughfareMethod;
 jmethodID AndroidAddress::jGetThoroughfareMethod;
 
 jclass AndroidGeckoSoftwareLayerClient::jGeckoSoftwareLayerClientClass = 0;
 jmethodID AndroidGeckoSoftwareLayerClient::jLockBufferMethod = 0;
 jmethodID AndroidGeckoSoftwareLayerClient::jUnlockBufferMethod = 0;
-jmethodID AndroidGeckoSoftwareLayerClient::jGetRenderOffsetMethod = 0;
 jmethodID AndroidGeckoSoftwareLayerClient::jBeginDrawingMethod = 0;
 jmethodID AndroidGeckoSoftwareLayerClient::jEndDrawingMethod = 0;
 jclass AndroidGeckoSurfaceView::jGeckoSurfaceViewClass = 0;
 jmethodID AndroidGeckoSurfaceView::jBeginDrawingMethod = 0;
 jmethodID AndroidGeckoSurfaceView::jEndDrawingMethod = 0;
 jmethodID AndroidGeckoSurfaceView::jDraw2DBitmapMethod = 0;
 jmethodID AndroidGeckoSurfaceView::jDraw2DBufferMethod = 0;
 jmethodID AndroidGeckoSurfaceView::jGetSoftwareDrawBitmapMethod = 0;
@@ -319,19 +318,18 @@ AndroidGeckoSoftwareLayerClient::InitGec
 #ifdef MOZ_JAVA_COMPOSITOR
     initInit();
 
     jGeckoSoftwareLayerClientClass =
         getClassGlobalRef("org/mozilla/gecko/gfx/GeckoSoftwareLayerClient");
 
     jLockBufferMethod = getMethod("lockBuffer", "()Ljava/nio/ByteBuffer;");
     jUnlockBufferMethod = getMethod("unlockBuffer", "()V");
-    jGetRenderOffsetMethod = getMethod("getRenderOffset", "()Landroid/graphics/Point;");
-    jBeginDrawingMethod = getMethod("beginDrawing", "(IILjava/lang/String;Z)Z");
-    jEndDrawingMethod = getMethod("endDrawing", "(IIII)V");
+    jBeginDrawingMethod = getMethod("beginDrawing", "(II)V");
+    jEndDrawingMethod = getMethod("endDrawing", "(IIIILjava/lang/String;Z)V");
 #endif
 }
 
 #undef initInit
 #undef initClassGlobalRef
 #undef getField
 #undef getMethod
 
@@ -615,38 +613,31 @@ void
 AndroidGeckoSoftwareLayerClient::UnlockBuffer()
 {
     NS_ASSERTION(!isNull(), "UnlockBuffer() called on null software layer client!");
     AndroidBridge::AutoLocalJNIFrame(1);
     JNI()->CallVoidMethod(wrapped_obj, jUnlockBufferMethod);
 }
 
 void
-AndroidGeckoSoftwareLayerClient::GetRenderOffset(nsIntPoint &aOffset)
-{
-    AndroidPoint offset(JNI(), JNI()->CallObjectMethod(wrapped_obj, jGetRenderOffsetMethod));
-    aOffset.x = offset.X();
-    aOffset.y = offset.Y();
-}
-
-bool
-AndroidGeckoSoftwareLayerClient::BeginDrawing(int aWidth, int aHeight, const nsAString &aMetadata, bool aHasDirectTexture)
+AndroidGeckoSoftwareLayerClient::BeginDrawing(int aWidth, int aHeight)
 {
     NS_ASSERTION(!isNull(), "BeginDrawing() called on null software layer client!");
     AndroidBridge::AutoLocalJNIFrame(1);
-    jstring jMetadata = JNI()->NewString(nsPromiseFlatString(aMetadata).get(), aMetadata.Length());
-    return JNI()->CallBooleanMethod(wrapped_obj, jBeginDrawingMethod, aWidth, aHeight, jMetadata, aHasDirectTexture);
+    return JNI()->CallVoidMethod(wrapped_obj, jBeginDrawingMethod, aWidth, aHeight);
 }
 
 void
-AndroidGeckoSoftwareLayerClient::EndDrawing(const nsIntRect &aRect)
+AndroidGeckoSoftwareLayerClient::EndDrawing(const nsIntRect &aRect, const nsAString &aMetadata, bool aHasDirectTexture)
 {
     NS_ASSERTION(!isNull(), "EndDrawing() called on null software layer client!");
     AndroidBridge::AutoLocalJNIFrame(1);
-    return JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod, aRect.x, aRect.y, aRect.width, aRect.height);
+    jstring jMetadata = JNI()->NewString(nsPromiseFlatString(aMetadata).get(), aMetadata.Length());
+    return JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod, aRect.x, aRect.y, aRect.width,
+                                 aRect.height, jMetadata, aHasDirectTexture);
 }
 
 jobject
 AndroidGeckoSurfaceView::GetSoftwareDrawBitmap()
 {
     return JNI()->CallObjectMethod(wrapped_obj, jGetSoftwareDrawBitmapMethod);
 }
 
--- a/widget/android/AndroidJavaWrappers.h
+++ b/widget/android/AndroidJavaWrappers.h
@@ -156,27 +156,25 @@ public:
      void Init(jobject jobj);
  
     AndroidGeckoSoftwareLayerClient() {}
     AndroidGeckoSoftwareLayerClient(jobject jobj) { Init(jobj); }
 
     jobject LockBuffer();
     unsigned char *LockBufferBits();
     void UnlockBuffer();
-    void GetRenderOffset(nsIntPoint &aOffset);
-    bool BeginDrawing(int aWidth, int aHeight, const nsAString &aMetadata, bool aHasDirectTexture);
-    void EndDrawing(const nsIntRect &aRect);
+    void BeginDrawing(int aWidth, int aHeight);
+    void EndDrawing(const nsIntRect &aRect, const nsAString &aMetadata, bool aHasDirectTexture);
 
 private:
     static jclass jGeckoSoftwareLayerClientClass;
     static jmethodID jLockBufferMethod;
     static jmethodID jUnlockBufferMethod;
 
 protected:
-     static jmethodID jGetRenderOffsetMethod;
      static jmethodID jBeginDrawingMethod;
      static jmethodID jEndDrawingMethod;
 };
 
 
 class AndroidGeckoSurfaceView : public WrappedJavaObject
 {
 public:
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -1190,32 +1190,23 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae)
     bool paintingSuppressed = false;
     if (metadataProvider) {
         metadataProvider->PaintingSuppressed(&paintingSuppressed);
     }
     if (paintingSuppressed) {
         return;
     }
 
-    nsAutoString metadata;
-    if (metadataProvider) {
-        metadataProvider->GetDrawMetadata(metadata);
-    }
-
     AndroidGeckoSoftwareLayerClient &client =
         AndroidBridge::Bridge()->GetSoftwareLayerClient();
-    if (!client.BeginDrawing(gAndroidBounds.width, gAndroidBounds.height, metadata, HasDirectTexture())) {
-        return;
-    }
-
-    nsIntPoint renderOffset;
-    client.GetRenderOffset(renderOffset);
+    client.BeginDrawing(gAndroidBounds.width, gAndroidBounds.height);
 
     nsIntRect dirtyRect = ae->Rect().Intersect(nsIntRect(0, 0, gAndroidBounds.width, gAndroidBounds.height));
 
+    nsAutoString metadata;
     unsigned char *bits = NULL;
     if (HasDirectTexture()) {
       if (sDirectTexture->Width() != gAndroidBounds.width ||
           sDirectTexture->Height() != gAndroidBounds.height) {
         sDirectTexture->Reallocate(gAndroidBounds.width, gAndroidBounds.height);
       }
 
       sDirectTexture->Lock(AndroidGraphicBuffer::UsageSoftwareWrite, dirtyRect, &bits);
@@ -1224,51 +1215,56 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae)
     }
     if (!bits) {
         ALOG("### Failed to lock buffer");
     } else {
         // If tile size is 0,0, we assume we only have a single tile
         int tileWidth = (gAndroidTileSize.width > 0) ? gAndroidTileSize.width : gAndroidBounds.width;
         int tileHeight = (gAndroidTileSize.height > 0) ? gAndroidTileSize.height : gAndroidBounds.height;
 
+        bool drawSuccess = true;
         int offset = 0;
 
-        // It is assumed that the buffer has been over-allocated so that not
-        // only is the tile-size constant, but that a render-offset of anything
-        // up to (but not including) the tile size could be accommodated.
-        for (int y = 0; y < gAndroidBounds.height + gAndroidTileSize.height; y += tileHeight) {
-            for (int x = 0; x < gAndroidBounds.width + gAndroidTileSize.width; x += tileWidth) {
+        for (int y = 0; y < gAndroidBounds.height; y += tileHeight) {
+            for (int x = 0; x < gAndroidBounds.width; x += tileWidth) {
+                int width = NS_MIN(tileWidth, gAndroidBounds.width - x);
+                int height = NS_MIN(tileHeight, gAndroidBounds.height - y);
 
                 nsRefPtr<gfxImageSurface> targetSurface =
                     new gfxImageSurface(bits + offset,
-                                        gfxIntSize(tileWidth, tileHeight),
-                                        tileWidth * 2,
+                                        gfxIntSize(width, height),
+                                        width * 2,
                                         gfxASurface::ImageFormatRGB16_565);
 
-                offset += tileWidth * tileHeight * 2;
+                offset += width * height * 2;
 
                 if (targetSurface->CairoStatus()) {
                     ALOG("### Failed to create a valid surface from the bitmap");
+                    drawSuccess = false;
                     break;
                 } else {
-                    targetSurface->SetDeviceOffset(gfxPoint(renderOffset.x - x,
-                                                            renderOffset.y - y));
+                    targetSurface->SetDeviceOffset(gfxPoint(-x, -y));
                     DrawTo(targetSurface, dirtyRect);
                 }
             }
         }
+
+        // Don't fill in the draw metadata on an unsuccessful draw
+        if (drawSuccess && metadataProvider) {
+            metadataProvider->GetDrawMetadata(metadata);
+        }
     }
 
     if (HasDirectTexture()) {
         sDirectTexture->Unlock();
     } else {
         client.UnlockBuffer();
     }
 
-    client.EndDrawing(dirtyRect);
+    client.EndDrawing(dirtyRect, metadata, HasDirectTexture());
     return;
 #endif
 
     if (!sSurfaceExists) {
         return;
     }
 
     AndroidGeckoSurfaceView& sview(AndroidBridge::Bridge()->SurfaceView());