From d70b23ac39b5b78eb66cd83a7161cf65e33ad15a Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Sun, 22 Jan 2012 09:40:37 +0000 Subject: [PATCH] Bug 717283 - Use tiles on-demand. r=pcwalton, snorp Instead of tying the tile-buffer in MultiTileLayer directly to the back-buffer of the page, make sure rendering is always aligned to the tile grid and use tiles on-demand. This makes better use of tiles when panning/zooming, and opens up the route for further optimisations. --- .../base/gfx/GeckoSoftwareLayerClient.java | 179 ++++--- mobile/android/base/gfx/Layer.java | 27 +- mobile/android/base/gfx/LayerRenderer.java | 18 + mobile/android/base/gfx/MultiTileLayer.java | 480 +++++++++++++----- mobile/android/base/gfx/RectUtils.java | 7 + mobile/android/base/gfx/SingleTileLayer.java | 24 +- mobile/android/base/gfx/TileLayer.java | 63 ++- mobile/android/chrome/content/browser.js | 1 + widget/android/AndroidJavaWrappers.cpp | 25 +- widget/android/AndroidJavaWrappers.h | 6 +- widget/android/nsWindow.cpp | 40 +- 11 files changed, 600 insertions(+), 270 deletions(-) diff --git a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java index eb82d649c5c..d319543175e 100644 --- a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java +++ b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java @@ -80,9 +80,15 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL private ByteBuffer mBuffer; private Layer mTileLayer; - /* The viewport rect that Gecko is currently displaying. */ + /* The viewport 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); @@ -108,6 +114,7 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL mScreenSize = new IntSize(0, 0); mBufferSize = new IntSize(0, 0); mFormat = CairoImage.FORMAT_RGB16_565; + mRenderOffset = new Point(0, 0); mCairoImage = new CairoImage() { @Override @@ -117,8 +124,6 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL @Override public int getFormat() { return mFormat; } }; - - mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE); } public int getWidth() { @@ -151,6 +156,8 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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 @@ -163,9 +170,9 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL sendResizeEventIfNecessary(); } - private void setHasDirectTexture(boolean hasDirectTexture) { - if (hasDirectTexture == mHasDirectTexture) - return; + private boolean setHasDirectTexture(boolean hasDirectTexture) { + if (mTileLayer != null && hasDirectTexture == mHasDirectTexture) + return false; mHasDirectTexture = hasDirectTexture; @@ -173,6 +180,7 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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; @@ -186,18 +194,62 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL // 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 void beginDrawing(int width, int height) { + 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; + 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); - // Reallocate the buffer if necessary + // 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); - // * 2 because it's a 16-bit buffer (so 2 bytes per pixel). - int size = mBufferSize.getArea() * 2; + // Reallocate the buffer if necessary + int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8; + int size = realBufferSize.getArea() * bpp; if (mBuffer == null || mBuffer.capacity() != size) { // Free the old buffer if (mBuffer != null) { @@ -208,39 +260,35 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL mBuffer = GeckoAppShell.allocateDirectBuffer(size); } } + + return true; } - private void updateViewport(String viewportDescription, final boolean onlyUpdatePageSize) { - try { - JSONObject viewportObject = new JSONObject(viewportDescription); + 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); - // 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 { - 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); + 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(); } } @@ -248,17 +296,17 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL * 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, String metadata, boolean hasDirectTexture) { + public void endDrawing(int x, int y, int width, int height) { synchronized (getLayerController()) { try { - updateViewport(metadata, !mUpdateViewportOnEndDraw); + updateViewport(!mUpdateViewportOnEndDraw); mUpdateViewportOnEndDraw = false; - Rect rect = new Rect(x, y, x + width, y + height); - setHasDirectTexture(hasDirectTexture); - - if (!mHasDirectTexture) + if (mTileLayer instanceof MultiTileLayer) { + Rect rect = new Rect(x, y, x + width, y + height); + rect.offset(mRenderOffset.x, mRenderOffset.y); ((MultiTileLayer)mTileLayer).invalidate(rect); + } } finally { endTransaction(mTileLayer); } @@ -277,29 +325,28 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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) { - // Calculate tile size - IntSize tileSize = new IntSize(Math.min(mBufferSize.width - x, TILE_SIZE.width), - Math.min(mBufferSize.height - y, TILE_SIZE.height)); - + for (int y = 0; y <= mBufferSize.height; y += TILE_SIZE.height) { + for (int x = 0; x <= mBufferSize.width; x += TILE_SIZE.width) { // Create a Bitmap from this tile - Bitmap tile = Bitmap.createBitmap(tileSize.width, tileSize.height, + Bitmap tile = Bitmap.createBitmap(TILE_SIZE.width, TILE_SIZE.height, CairoUtils.cairoFormatTobitmapConfig(mFormat)); tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer()); // Copy the tile to the master Bitmap and recycle it - c.drawBitmap(tile, x, y, null); + c.drawBitmap(tile, x - mRenderOffset.x, y - mRenderOffset.y, null); tile.recycle(); // Progress the buffer to the next tile - tileBuffer.position(tileSize.getArea() * bpp); + tileBuffer.position(TILE_SIZE.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); @@ -311,12 +358,8 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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"); } @@ -336,6 +379,10 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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. @@ -370,7 +417,7 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL // Round up depending on layer implementation to remove texture wastage if (mTileLayer instanceof MultiTileLayer) { - // Round to the next multiple of the tile size, respecting maximum texture size + // Round to the next multiple of the tile 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); @@ -381,7 +428,7 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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 + // Round to next power of two until we have NPOT texture support, respecting maximum texture size 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))); } @@ -452,6 +499,16 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL 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); + } } } } diff --git a/mobile/android/base/gfx/Layer.java b/mobile/android/base/gfx/Layer.java index fc15f48026d..4c332866754 100644 --- a/mobile/android/base/gfx/Layer.java +++ b/mobile/android/base/gfx/Layer.java @@ -63,23 +63,31 @@ public abstract class Layer { /** * Updates the layer. This returns false if there is still work to be done - * after this update. + * 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. */ public final boolean update(GL10 gl, RenderContext context) { + boolean startTransaction = true; if (mTransactionLock.isHeldByCurrentThread()) { - throw new RuntimeException("draw() called while transaction lock held by this " + - "thread?!"); + startTransaction = false; } - if (mTransactionLock.tryLock()) { - try { - return performUpdates(gl, context); - } finally { + // 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; mTransactionLock.unlock(); } } - - return false; } /** Subclasses override this function to draw the layer. */ @@ -109,7 +117,6 @@ public abstract class Layer { mTransactionLock.lock(); mView = aView; mInTransaction = true; - mNewResolution = mResolution; } public void beginTransaction() { diff --git a/mobile/android/base/gfx/LayerRenderer.java b/mobile/android/base/gfx/LayerRenderer.java index fe9cdb92b0e..6d96f04da39 100644 --- a/mobile/android/base/gfx/LayerRenderer.java +++ b/mobile/android/base/gfx/LayerRenderer.java @@ -101,12 +101,30 @@ public class LayerRenderer implements GLSurfaceView.Renderer { 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/--"); diff --git a/mobile/android/base/gfx/MultiTileLayer.java b/mobile/android/base/gfx/MultiTileLayer.java index 6bcbe6b5b8d..ef4652f1567 100644 --- a/mobile/android/base/gfx/MultiTileLayer.java +++ b/mobile/android/base/gfx/MultiTileLayer.java @@ -37,15 +37,21 @@ 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.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.ListIterator; import javax.microedition.khronos.opengles.GL10; /** @@ -57,9 +63,12 @@ public class MultiTileLayer extends Layer { private static final String LOGTAG = "GeckoMultiTileLayer"; private final CairoImage mImage; - private IntSize mTileSize; + private final IntSize mTileSize; private IntSize mBufferSize; - private final ArrayList mTiles; + private Region mDirtyRegion; + private Region mValidRegion; + private final LinkedList mTiles; + private final HashMap mPositionHash; public MultiTileLayer(CairoImage image, IntSize tileSize) { super(); @@ -67,27 +76,37 @@ public class MultiTileLayer extends Layer { mImage = image; mTileSize = tileSize; mBufferSize = new IntSize(0, 0); - mTiles = new ArrayList(); + mDirtyRegion = new Region(); + mValidRegion = new Region(); + mTiles = new LinkedList(); + mPositionHash = new HashMap(); } + /** + * 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"); - - for (SubTile layer : mTiles) { - IntSize tileSize = layer.getSize(); - Rect tileRect = new Rect(layer.x, layer.y, layer.x + tileSize.width, layer.y + tileSize.height); - - if (tileRect.intersect(dirtyRect)) { - tileRect.offset(-layer.x, -layer.y); - layer.invalidate(tileRect); - } } + + mDirtyRegion.union(dirtyRect); + mValidRegion.union(dirtyRect); } - public void invalidate() { - for (SubTile layer : mTiles) - layer.invalidate(); + /** + * 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(); } @Override @@ -95,61 +114,104 @@ public class MultiTileLayer extends Layer { 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; - // 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; + mBufferSize = size; - 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); + // 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); - return tileBuffer; - } + 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); - @Override - public IntSize getSize() { - return layerSize; - } - - @Override - public int getFormat() { - return format; - } - }; - - mTiles.add(new SubTile(subImage, x, y)); - offset += layerSize.getArea() * bpp; + // 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); } } - // Set tile origins and resolution - refreshTileMetrics(getOrigin(), getResolution(), false); + // A buffer size probably means a layout change, so invalidate all tiles. + invalidateTiles(); + } - mBufferSize = size; + /** + * 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); + + // 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(); + } } @Override @@ -158,96 +220,167 @@ public class MultiTileLayer extends Layer { validateTiles(); - // Iterate over the tiles and decide which ones we'll be drawing - int dirtyTiles = 0; - boolean screenUpdateDone = false; - SubTile firstDirtyTile = null; - for (SubTile 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); + // Bail out early if we have nothing to do. + if (mDirtyRegion.isEmpty() || mTiles.isEmpty()) { + return true; + } - RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize())); - boolean isDirty = layer.isDirty(); + // 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; + } - 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; - } + // 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); + + // 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 invalidTiles = new LinkedList(); + Rect bounds = mValidRegion.getBounds(); + for (ListIterator i = mTiles.listIterator(); i.hasNext();) { + SubTile tile = i.next(); + + if (tile.key == null) { + continue; } - // 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); + RectF tileBounds = tile.getBounds(context, new FloatSize(tile.getSize())); + Rect tilespaceTileBounds = + RectUtils.round(RectUtils.scale(tileBounds, scaleFactor)); + tilespaceTileBounds.offset(-origin.x, -origin.y); + + // 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); + + // 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)); + + // 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); + + // 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); + + // 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(); + } + } } - return (dirtyTiles == 0); - } + // Remove the update region from the dirty region + mDirtyRegion.op(updateRegion, Region.Op.XOR); - private void refreshTileMetrics(Point origin, float resolution, boolean inTransaction) { - IntSize size = getSize(); - for (SubTile layer : mTiles) { - if (!inTransaction) - layer.beginTransaction(null); - - if (origin != null) - layer.setOrigin(new Point(origin.x + layer.x, origin.y + layer.y)); - if (resolution >= 0.0f) - layer.setResolution(resolution); - - if (!inTransaction) - layer.endTransaction(); - } - } - - @Override - public void setOrigin(Point newOrigin) { - super.setOrigin(newOrigin); - refreshTileMetrics(newOrigin, -1, true); - } - - @Override - public void setResolution(float newResolution) { - super.setResolution(newResolution); - refreshTileMetrics(null, newResolution, true); + return mDirtyRegion.isEmpty(); } @Override public void beginTransaction(LayerView aView) { super.beginTransaction(aView); - for (SubTile layer : mTiles) + for (SubTile layer : mTiles) { layer.beginTransaction(aView); + } } @Override public void endTransaction() { - for (SubTile layer : mTiles) + for (SubTile layer : mTiles) { layer.endTransaction(); + } super.endTransaction(); } @@ -255,11 +388,6 @@ public class MultiTileLayer extends Layer { @Override public void draw(RenderContext context) { for (SubTile 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)) @@ -267,14 +395,90 @@ public class MultiTileLayer extends Layer { } } - class SubTile extends SingleTileLayer { + /** + * 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 SubTile(CairoImage mImage, int mX, int mY) { - super(mImage); - x = mX; - y = mY; + 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(); } } } diff --git a/mobile/android/base/gfx/RectUtils.java b/mobile/android/base/gfx/RectUtils.java index 5d1ab6a838c..33877bf188e 100644 --- a/mobile/android/base/gfx/RectUtils.java +++ b/mobile/android/base/gfx/RectUtils.java @@ -99,11 +99,18 @@ public final class RectUtils { 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()); } diff --git a/mobile/android/base/gfx/SingleTileLayer.java b/mobile/android/base/gfx/SingleTileLayer.java index 39d063d5795..940527df2a9 100644 --- a/mobile/android/base/gfx/SingleTileLayer.java +++ b/mobile/android/base/gfx/SingleTileLayer.java @@ -43,6 +43,7 @@ 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; @@ -58,6 +59,8 @@ import javax.microedition.khronos.opengles.GL10; * 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) { @@ -71,6 +74,11 @@ public class SingleTileLayer extends TileLayer { 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; @@ -79,13 +87,27 @@ public class SingleTileLayer extends TileLayer { 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)); - cropRect = new int[] { 0, size.height, size.width, -size.height }; + + 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() }; } GLES11.glTexParameteriv(GL10.GL_TEXTURE_2D, GLES11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, diff --git a/mobile/android/base/gfx/TileLayer.java b/mobile/android/base/gfx/TileLayer.java index ad52229cfbc..e99315142ed 100644 --- a/mobile/android/base/gfx/TileLayer.java +++ b/mobile/android/base/gfx/TileLayer.java @@ -59,14 +59,14 @@ public abstract class TileLayer extends Layer { private final CairoImage mImage; private final boolean mRepeat; private IntSize mSize; - private boolean mSkipTextureUpdate; + private Rect mValidTextureRect; private int[] mTextureIDs; public TileLayer(boolean repeat, CairoImage image) { mRepeat = repeat; mImage = image; mSize = new IntSize(0, 0); - mSkipTextureUpdate = false; + mValidTextureRect = new Rect(); IntSize bufferSize = mImage.getSize(); mDirtyRect = new Rect(); @@ -132,22 +132,27 @@ public abstract class TileLayer extends Layer { } } - /** Tells the tile not to update the texture on the next update. */ - public void setSkipTextureUpdate(boolean skip) { - mSkipTextureUpdate = skip; + /** + * Tells the tile that its texture contents are invalid. This will also + * clear any invalidated area. + */ + public void invalidateTexture() { + mValidTextureRect.setEmpty(); + mDirtyRect.setEmpty(); } - public boolean getSkipTextureUpdate() { - return mSkipTextureUpdate; + /** + * 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; } @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); @@ -155,23 +160,20 @@ public abstract class TileLayer extends Layer { if (!mImage.getSize().isPositive()) return true; - // If we haven't allocated a texture, assume the whole region is dirty - if (mTextureIDs == null) - uploadFullTexture(gl); - else - uploadDirtyRect(gl, mDirtyRect); - + // Update the dirty region + 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) { + IntSize bufferSize = mImage.getSize(); + Rect bufferRect = new Rect(0, 0, bufferSize.width, bufferSize.height); + + // Make sure the dirty region intersects with the buffer + dirtyRect.intersect(bufferRect); + // If we have nothing to upload, just return for now if (dirtyRect.isEmpty()) return; @@ -181,6 +183,11 @@ public abstract class TileLayer extends Layer { 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) { @@ -189,15 +196,12 @@ public abstract class TileLayer extends Layer { 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.contains(bufferRect)) { + if (newlyCreated || dirtyRect.equals(bufferRect)) { if (mSize.equals(bufferSize)) { gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, mSize.height, 0, glInfo.format, glInfo.type, imageBuffer); @@ -211,11 +215,6 @@ public abstract class TileLayer extends Layer { } } - // 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, @@ -232,8 +231,8 @@ public abstract class TileLayer extends Layer { } viewBuffer.position(position); - gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top, bufferSize.width, - Math.min(bufferSize.height - dirtyRect.top, dirtyRect.height()), + gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top, + bufferSize.width, dirtyRect.height(), glInfo.format, glInfo.type, viewBuffer); } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 29a458714a4..00fe3ad2c78 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -2023,6 +2023,7 @@ Tab.prototype = { // 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); } diff --git a/widget/android/AndroidJavaWrappers.cpp b/widget/android/AndroidJavaWrappers.cpp index 6d348fd2bd6..475eb7d6151 100644 --- a/widget/android/AndroidJavaWrappers.cpp +++ b/widget/android/AndroidJavaWrappers.cpp @@ -107,6 +107,7 @@ 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; @@ -323,8 +324,9 @@ AndroidGeckoSoftwareLayerClient::InitGeckoSoftwareLayerClientClass(JNIEnv *jEnv) jLockBufferMethod = getMethod("lockBuffer", "()Ljava/nio/ByteBuffer;"); jUnlockBufferMethod = getMethod("unlockBuffer", "()V"); - jBeginDrawingMethod = getMethod("beginDrawing", "(II)V"); - jEndDrawingMethod = getMethod("endDrawing", "(IIIILjava/lang/String;Z)V"); + jGetRenderOffsetMethod = getMethod("getRenderOffset", "()Landroid/graphics/Point;"); + jBeginDrawingMethod = getMethod("beginDrawing", "(IILjava/lang/String;Z)Z"); + jEndDrawingMethod = getMethod("endDrawing", "(IIII)V"); #endif } @@ -618,21 +620,28 @@ AndroidGeckoSoftwareLayerClient::UnlockBuffer() } void -AndroidGeckoSoftwareLayerClient::BeginDrawing(int aWidth, int aHeight) +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) { NS_ASSERTION(!isNull(), "BeginDrawing() called on null software layer client!"); AndroidBridge::AutoLocalJNIFrame(1); - return JNI()->CallVoidMethod(wrapped_obj, jBeginDrawingMethod, aWidth, aHeight); + jstring jMetadata = JNI()->NewString(nsPromiseFlatString(aMetadata).get(), aMetadata.Length()); + return JNI()->CallBooleanMethod(wrapped_obj, jBeginDrawingMethod, aWidth, aHeight, jMetadata, aHasDirectTexture); } void -AndroidGeckoSoftwareLayerClient::EndDrawing(const nsIntRect &aRect, const nsAString &aMetadata, bool aHasDirectTexture) +AndroidGeckoSoftwareLayerClient::EndDrawing(const nsIntRect &aRect) { NS_ASSERTION(!isNull(), "EndDrawing() called on null software layer client!"); AndroidBridge::AutoLocalJNIFrame(1); - 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); + return JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod, aRect.x, aRect.y, aRect.width, aRect.height); } jobject diff --git a/widget/android/AndroidJavaWrappers.h b/widget/android/AndroidJavaWrappers.h index 1bdd40a0bdd..17832468cdc 100644 --- a/widget/android/AndroidJavaWrappers.h +++ b/widget/android/AndroidJavaWrappers.h @@ -161,8 +161,9 @@ public: jobject LockBuffer(); unsigned char *LockBufferBits(); void UnlockBuffer(); - void BeginDrawing(int aWidth, int aHeight); - void EndDrawing(const nsIntRect &aRect, const nsAString &aMetadata, bool aHasDirectTexture); + void GetRenderOffset(nsIntPoint &aOffset); + bool BeginDrawing(int aWidth, int aHeight, const nsAString &aMetadata, bool aHasDirectTexture); + void EndDrawing(const nsIntRect &aRect); private: static jclass jGeckoSoftwareLayerClientClass; @@ -170,6 +171,7 @@ private: static jmethodID jUnlockBufferMethod; protected: + static jmethodID jGetRenderOffsetMethod; static jmethodID jBeginDrawingMethod; static jmethodID jEndDrawingMethod; }; diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp index b07846af637..b949fd71d48 100644 --- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -1195,13 +1195,22 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae) return; } + nsAutoString metadata; + if (metadataProvider) { + metadataProvider->GetDrawMetadata(metadata); + } + AndroidGeckoSoftwareLayerClient &client = AndroidBridge::Bridge()->GetSoftwareLayerClient(); - client.BeginDrawing(gAndroidBounds.width, gAndroidBounds.height); + if (!client.BeginDrawing(gAndroidBounds.width, gAndroidBounds.height, metadata, HasDirectTexture())) { + return; + } + + nsIntPoint renderOffset; + client.GetRenderOffset(renderOffset); 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 || @@ -1220,37 +1229,32 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae) int tileWidth = (gAndroidTileSize.width > 0) ? gAndroidTileSize.width : gAndroidBounds.width; int tileHeight = (gAndroidTileSize.height > 0) ? gAndroidTileSize.height : gAndroidBounds.height; - bool drawSuccess = true; int offset = 0; - 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); + // 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) { nsRefPtr targetSurface = new gfxImageSurface(bits + offset, - gfxIntSize(width, height), - width * 2, + gfxIntSize(tileWidth, tileHeight), + tileWidth * 2, gfxASurface::ImageFormatRGB16_565); - offset += width * height * 2; + offset += tileWidth * tileHeight * 2; if (targetSurface->CairoStatus()) { ALOG("### Failed to create a valid surface from the bitmap"); - drawSuccess = false; break; } else { - targetSurface->SetDeviceOffset(gfxPoint(-x, -y)); + targetSurface->SetDeviceOffset(gfxPoint(renderOffset.x - x, + renderOffset.y - y)); DrawTo(targetSurface, dirtyRect); } } } - - // Don't fill in the draw metadata on an unsuccessful draw - if (drawSuccess && metadataProvider) { - metadataProvider->GetDrawMetadata(metadata); - } } if (HasDirectTexture()) { @@ -1259,7 +1263,7 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae) client.UnlockBuffer(); } - client.EndDrawing(dirtyRect, metadata, HasDirectTexture()); + client.EndDrawing(dirtyRect); return; #endif