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.
This commit is contained in:
Chris Lord 2012-01-22 09:40:37 +00:00
parent 9426018c8c
commit d70b23ac39
11 changed files with 600 additions and 270 deletions

View File

@ -80,9 +80,15 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
private ByteBuffer mBuffer; private ByteBuffer mBuffer;
private Layer mTileLayer; private Layer mTileLayer;
/* The viewport rect that Gecko is currently displaying. */ /* The viewport that Gecko is currently displaying. */
private ViewportMetrics mGeckoViewport; 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 CairoImage mCairoImage;
private static final IntSize TILE_SIZE = new IntSize(256, 256); 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); mScreenSize = new IntSize(0, 0);
mBufferSize = new IntSize(0, 0); mBufferSize = new IntSize(0, 0);
mFormat = CairoImage.FORMAT_RGB16_565; mFormat = CairoImage.FORMAT_RGB16_565;
mRenderOffset = new Point(0, 0);
mCairoImage = new CairoImage() { mCairoImage = new CairoImage() {
@Override @Override
@ -117,8 +124,6 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
@Override @Override
public int getFormat() { return mFormat; } public int getFormat() { return mFormat; }
}; };
mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE);
} }
public int getWidth() { public int getWidth() {
@ -151,6 +156,8 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
GeckoAppShell.registerGeckoEventListener("Viewport:UpdateAndDraw", this); GeckoAppShell.registerGeckoEventListener("Viewport:UpdateAndDraw", this);
GeckoAppShell.registerGeckoEventListener("Viewport:UpdateLater", 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 // This needs to happen before a call to sendResizeEventIfNecessary
// happens, but only needs to be called once. As that is only called by // 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(); sendResizeEventIfNecessary();
} }
private void setHasDirectTexture(boolean hasDirectTexture) { private boolean setHasDirectTexture(boolean hasDirectTexture) {
if (hasDirectTexture == mHasDirectTexture) if (mTileLayer != null && hasDirectTexture == mHasDirectTexture)
return; return false;
mHasDirectTexture = hasDirectTexture; mHasDirectTexture = hasDirectTexture;
@ -173,6 +180,7 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
if (mHasDirectTexture) { if (mHasDirectTexture) {
mTileLayer = new WidgetTileLayer(mCairoImage); mTileLayer = new WidgetTileLayer(mCairoImage);
tileSize = new IntSize(0, 0); tileSize = new IntSize(0, 0);
mRenderOffset.set(0, 0);
} else { } else {
mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE); mTileLayer = new MultiTileLayer(mCairoImage, TILE_SIZE);
tileSize = 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 // Force a resize event to be sent because the results of this
// are different depending on what tile system we're using // are different depending on what tile system we're using
sendResizeEventIfNecessary(true); 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); 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) { if (mBufferSize.width != width || mBufferSize.height != height) {
mBufferSize = new IntSize(width, 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). // Reallocate the buffer if necessary
int size = mBufferSize.getArea() * 2; int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;
int size = realBufferSize.getArea() * bpp;
if (mBuffer == null || mBuffer.capacity() != size) { if (mBuffer == null || mBuffer.capacity() != size) {
// Free the old buffer // Free the old buffer
if (mBuffer != null) { if (mBuffer != null) {
@ -208,39 +260,35 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
mBuffer = GeckoAppShell.allocateDirectBuffer(size); mBuffer = GeckoAppShell.allocateDirectBuffer(size);
} }
} }
return true;
} }
private void updateViewport(String viewportDescription, final boolean onlyUpdatePageSize) { private void updateViewport(final boolean onlyUpdatePageSize) {
try { // save and restore the viewport size stored in java; never let the
JSONObject viewportObject = new JSONObject(viewportDescription); // 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 LayerController controller = getLayerController();
// JS-side viewport dimensions override the java-side ones because PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin();
// java is the One True Source of this information, and allowing JS Point tileOrigin = PointUtils.round(displayportOrigin);
// to override can lead to race conditions where this data gets clobbered. tileOrigin.offset(-mRenderOffset.x, -mRenderOffset.y);
FloatSize viewportSize = getLayerController().getViewportSize(); mTileLayer.setOrigin(tileOrigin);
mGeckoViewport = new ViewportMetrics(viewportObject); mTileLayer.setResolution(mGeckoViewport.getZoomFactor());
mGeckoViewport.setSize(viewportSize);
LayerController controller = getLayerController(); if (onlyUpdatePageSize) {
PointF displayportOrigin = mGeckoViewport.getDisplayportOrigin(); // Don't adjust page size when zooming unless zoom levels are
mTileLayer.setOrigin(PointUtils.round(displayportOrigin)); // approximately equal.
mTileLayer.setResolution(mGeckoViewport.getZoomFactor()); if (FloatUtils.fuzzyEquals(controller.getZoomFactor(),
mGeckoViewport.getZoomFactor()))
if (onlyUpdatePageSize) { controller.setPageSize(mGeckoViewport.getPageSize());
// Don't adjust page size when zooming unless zoom levels are } else {
// approximately equal. controller.setViewportMetrics(mGeckoViewport);
if (FloatUtils.fuzzyEquals(controller.getZoomFactor(), controller.abortPanZoomAnimation();
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);
} }
} }
@ -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 * TODO: Would be cleaner if this took an android.graphics.Rect instead, but that would require
* a little more JNI magic. * 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()) { synchronized (getLayerController()) {
try { try {
updateViewport(metadata, !mUpdateViewportOnEndDraw); updateViewport(!mUpdateViewportOnEndDraw);
mUpdateViewportOnEndDraw = false; mUpdateViewportOnEndDraw = false;
Rect rect = new Rect(x, y, x + width, y + height);
setHasDirectTexture(hasDirectTexture); if (mTileLayer instanceof MultiTileLayer) {
Rect rect = new Rect(x, y, x + width, y + height);
if (!mHasDirectTexture) rect.offset(mRenderOffset.x, mRenderOffset.y);
((MultiTileLayer)mTileLayer).invalidate(rect); ((MultiTileLayer)mTileLayer).invalidate(rect);
}
} finally { } finally {
endTransaction(mTileLayer); endTransaction(mTileLayer);
} }
@ -277,29 +325,28 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
ByteBuffer tileBuffer = mBuffer.slice(); ByteBuffer tileBuffer = mBuffer.slice();
int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8; int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8;
for (int y = 0; y < 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) { 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 // 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)); CairoUtils.cairoFormatTobitmapConfig(mFormat));
tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer()); tile.copyPixelsFromBuffer(tileBuffer.asIntBuffer());
// Copy the tile to the master Bitmap and recycle it // 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(); tile.recycle();
// Progress the buffer to the next tile // Progress the buffer to the next tile
tileBuffer.position(tileSize.getArea() * bpp); tileBuffer.position(TILE_SIZE.getArea() * bpp);
tileBuffer = tileBuffer.slice(); tileBuffer = tileBuffer.slice();
} }
} }
} }
public Bitmap getBitmap() { public Bitmap getBitmap() {
if (mTileLayer == null)
return null;
// Begin a tile transaction, otherwise the buffer can be destroyed while // Begin a tile transaction, otherwise the buffer can be destroyed while
// we're reading from it. // we're reading from it.
beginTransaction(mTileLayer); beginTransaction(mTileLayer);
@ -311,12 +358,8 @@ public class GeckoSoftwareLayerClient extends LayerClient implements GeckoEventL
if (mTileLayer instanceof MultiTileLayer) { if (mTileLayer instanceof MultiTileLayer) {
b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height, b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height,
CairoUtils.cairoFormatTobitmapConfig(mFormat)); CairoUtils.cairoFormatTobitmapConfig(mFormat));
copyPixelsFromMultiTileLayer(b); copyPixelsFromMultiTileLayer(b);
} else if (mTileLayer instanceof SingleTileLayer) {
b = Bitmap.createBitmap(mBufferSize.width, mBufferSize.height,
CairoUtils.cairoFormatTobitmapConfig(mFormat));
b.copyPixelsFromBuffer(mBuffer.asIntBuffer());
} else { } else {
Log.w(LOGTAG, "getBitmap() called on a layer (" + mTileLayer + ") we don't know how to get a bitmap from"); 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; return mBuffer;
} }
public Point getRenderOffset() {
return mRenderOffset;
}
/** /**
* Gecko calls this function to signal that it is done with the back buffer. After this call, * 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. * 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 // Round up depending on layer implementation to remove texture wastage
if (mTileLayer instanceof MultiTileLayer) { 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, 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); ((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) if (mScreenSize.width > maxSize || mScreenSize.height > maxSize)
throw new RuntimeException("Screen size of " + mScreenSize + " larger than maximum texture size of " + 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)), 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))); 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)); GeckoAppShell.sendEventToGecko(new GeckoEvent(GeckoEvent.DRAW, rect));
} else if ("Viewport:UpdateLater".equals(event)) { } else if ("Viewport:UpdateLater".equals(event)) {
mUpdateViewportOnEndDraw = true; 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);
}
} }
} }
} }

View File

@ -63,23 +63,31 @@ public abstract class Layer {
/** /**
* Updates the layer. This returns false if there is still work to be done * 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) { public final boolean update(GL10 gl, RenderContext context) {
boolean startTransaction = true;
if (mTransactionLock.isHeldByCurrentThread()) { if (mTransactionLock.isHeldByCurrentThread()) {
throw new RuntimeException("draw() called while transaction lock held by this " + startTransaction = false;
"thread?!");
} }
if (mTransactionLock.tryLock()) { // If we're not already in a transaction and we can't acquire the lock,
try { // bail out.
return performUpdates(gl, context); if (startTransaction && !mTransactionLock.tryLock()) {
} finally { return false;
}
mInTransaction = true;
try {
return performUpdates(gl, context);
} finally {
if (startTransaction) {
mInTransaction = false;
mTransactionLock.unlock(); mTransactionLock.unlock();
} }
} }
return false;
} }
/** Subclasses override this function to draw the layer. */ /** Subclasses override this function to draw the layer. */
@ -109,7 +117,6 @@ public abstract class Layer {
mTransactionLock.lock(); mTransactionLock.lock();
mView = aView; mView = aView;
mInTransaction = true; mInTransaction = true;
mNewResolution = mResolution;
} }
public void beginTransaction() { public void beginTransaction() {

View File

@ -101,12 +101,30 @@ public class LayerRenderer implements GLSurfaceView.Renderer {
CairoImage backgroundImage = new BufferedCairoImage(controller.getBackgroundPattern()); CairoImage backgroundImage = new BufferedCairoImage(controller.getBackgroundPattern());
mBackgroundLayer = new SingleTileLayer(true, backgroundImage); mBackgroundLayer = new SingleTileLayer(true, backgroundImage);
mBackgroundLayer.beginTransaction(null);
try {
mBackgroundLayer.invalidate();
} finally {
mBackgroundLayer.endTransaction();
}
CairoImage checkerboardImage = new BufferedCairoImage(controller.getCheckerboardPattern()); CairoImage checkerboardImage = new BufferedCairoImage(controller.getCheckerboardPattern());
mCheckerboardLayer = new SingleTileLayer(true, checkerboardImage); mCheckerboardLayer = new SingleTileLayer(true, checkerboardImage);
mCheckerboardLayer.beginTransaction(null);
try {
mCheckerboardLayer.invalidate();
} finally {
mCheckerboardLayer.endTransaction();
}
CairoImage shadowImage = new BufferedCairoImage(controller.getShadowPattern()); CairoImage shadowImage = new BufferedCairoImage(controller.getShadowPattern());
mShadowLayer = new NinePatchTileLayer(shadowImage); 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); IntSize frameRateLayerSize = new IntSize(FRAME_RATE_METER_WIDTH, FRAME_RATE_METER_HEIGHT);
mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--"); mFrameRateLayer = TextLayer.create(frameRateLayerSize, "-- ms/--");

View File

@ -37,15 +37,21 @@
package org.mozilla.gecko.gfx; package org.mozilla.gecko.gfx;
import org.mozilla.gecko.FloatUtils;
import org.mozilla.gecko.gfx.CairoImage; import org.mozilla.gecko.gfx.CairoImage;
import org.mozilla.gecko.gfx.IntSize; import org.mozilla.gecko.gfx.IntSize;
import org.mozilla.gecko.gfx.SingleTileLayer; import org.mozilla.gecko.gfx.SingleTileLayer;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Region;
import android.util.Log; import android.util.Log;
import java.lang.Long;
import java.nio.ByteBuffer; 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; import javax.microedition.khronos.opengles.GL10;
/** /**
@ -57,9 +63,12 @@ public class MultiTileLayer extends Layer {
private static final String LOGTAG = "GeckoMultiTileLayer"; private static final String LOGTAG = "GeckoMultiTileLayer";
private final CairoImage mImage; private final CairoImage mImage;
private IntSize mTileSize; private final IntSize mTileSize;
private IntSize mBufferSize; private IntSize mBufferSize;
private final ArrayList<SubTile> mTiles; private Region mDirtyRegion;
private Region mValidRegion;
private final LinkedList<SubTile> mTiles;
private final HashMap<Long, SubTile> mPositionHash;
public MultiTileLayer(CairoImage image, IntSize tileSize) { public MultiTileLayer(CairoImage image, IntSize tileSize) {
super(); super();
@ -67,27 +76,37 @@ public class MultiTileLayer extends Layer {
mImage = image; mImage = image;
mTileSize = tileSize; mTileSize = tileSize;
mBufferSize = new IntSize(0, 0); mBufferSize = new IntSize(0, 0);
mTiles = new ArrayList<SubTile>(); mDirtyRegion = new Region();
mValidRegion = new Region();
mTiles = new LinkedList<SubTile>();
mPositionHash = new HashMap<Long, SubTile>();
} }
/**
* 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) { public void invalidate(Rect dirtyRect) {
if (!inTransaction()) if (!inTransaction()) {
throw new RuntimeException("invalidate() is only valid inside a transaction"); 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) * Invalidates the backing buffer. Data will not be uploaded from an invalid
layer.invalidate(); * 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 @Override
@ -95,61 +114,104 @@ public class MultiTileLayer extends Layer {
return mImage.getSize(); return mImage.getSize();
} }
/**
* Makes sure there are enough tiles to accommodate the buffer image.
*/
private void validateTiles() { private void validateTiles() {
IntSize size = getSize(); IntSize size = getSize();
if (size.equals(mBufferSize)) if (size.equals(mBufferSize))
return; return;
// Regenerate tiles mBufferSize = size;
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;
CairoImage subImage = new CairoImage() { // Shrink/grow tile pool
@Override int nTiles = (Math.round(size.width / (float)mTileSize.width) + 1) *
public ByteBuffer getBuffer() { (Math.round(size.height / (float)mTileSize.height) + 1);
// Create a ByteBuffer that shares the data of the original if (mTiles.size() < nTiles) {
// buffer, but is positioned and limited so that only the Log.i(LOGTAG, "Tile pool growing from " + mTiles.size() + " to " + nTiles);
// tile data is accessible.
buffer.position(tileOffset);
ByteBuffer tileBuffer = buffer.slice();
tileBuffer.limit(layerSize.getArea() * bpp);
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 // Remove tiles from the beginning of the list, as these are
public IntSize getSize() { // least recently used tiles
return layerSize; for (int i = mTiles.size(); i > nTiles; i--) {
} SubTile tile = mTiles.get(0);
if (tile.key != null) {
@Override mPositionHash.remove(tile.key);
public int getFormat() { }
return format; mTiles.remove(0);
}
};
mTiles.add(new SubTile(subImage, x, y));
offset += layerSize.getArea() * bpp;
} }
} }
// Set tile origins and resolution // A buffer size probably means a layout change, so invalidate all tiles.
refreshTileMetrics(getOrigin(), getResolution(), false); 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 @Override
@ -158,96 +220,167 @@ public class MultiTileLayer extends Layer {
validateTiles(); validateTiles();
// Iterate over the tiles and decide which ones we'll be drawing // Bail out early if we have nothing to do.
int dirtyTiles = 0; if (mDirtyRegion.isEmpty() || mTiles.isEmpty()) {
boolean screenUpdateDone = false; return true;
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);
RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize())); // Check that we're capable of updating from this origin.
boolean isDirty = layer.isDirty(); 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) { // Transform the viewport into tile-space so we can see what part of the
if (!RectF.intersects(layerBounds, context.viewport)) { // dirty region intersects with it.
if (firstDirtyTile == null) // We update any tiles intersecting with the screen before tiles
firstDirtyTile = layer; // intersecting with the viewport.
dirtyTiles ++; // XXX Maybe we want to to split this update even further to update
invalid = true; // checkerboard area before updating screen regions with old data.
} else { // Note that this could provide inconsistent views, so we may not
// This tile intersects with the screen and is dirty, // want to do this.
// update it immediately. Rect tilespaceViewport;
layer.setSkipTextureUpdate(false); float scaleFactor = getResolution() / context.zoomFactor;
screenUpdateDone = true; tilespaceViewport = RectUtils.roundOut(RectUtils.scale(context.viewport, scaleFactor));
layer.performUpdates(gl, context); tilespaceViewport.offset(-origin.x, -origin.y);
invalid = false;
} // 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;
} }
// We use the SkipTextureUpdate flag as a marker of a tile's RectF tileBounds = tile.getBounds(context, new FloatSize(tile.getSize()));
// validity. This is required, as sometimes layers are drawn Rect tilespaceTileBounds =
// without updating first, and we mustn't draw tiles that have RectUtils.round(RectUtils.scale(tileBounds, scaleFactor));
// been marked as invalid that we haven't updated. tilespaceTileBounds.offset(-origin.x, -origin.y);
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 // Push invalid tiles to the head of the queue so they get used first
// a single tile that doesn't (if there are any). This has the effect mTiles.addAll(0, invalidTiles);
// of spreading out non-critical texture upload over time, and smoothing
// upload-related hitches. // Update tiles
if (!screenUpdateDone && firstDirtyTile != null) { // Note, it's <= as the buffer is over-allocated due to render-offsetting.
firstDirtyTile.setSkipTextureUpdate(false); for (int y = origin.y; y <= origin.y + mBufferSize.height; y += mTileSize.height) {
firstDirtyTile.performUpdates(gl, context); for (int x = origin.x; x <= origin.x + mBufferSize.width; x += mTileSize.width) {
dirtyTiles --; // 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) { return mDirtyRegion.isEmpty();
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);
} }
@Override @Override
public void beginTransaction(LayerView aView) { public void beginTransaction(LayerView aView) {
super.beginTransaction(aView); super.beginTransaction(aView);
for (SubTile layer : mTiles) for (SubTile layer : mTiles) {
layer.beginTransaction(aView); layer.beginTransaction(aView);
}
} }
@Override @Override
public void endTransaction() { public void endTransaction() {
for (SubTile layer : mTiles) for (SubTile layer : mTiles) {
layer.endTransaction(); layer.endTransaction();
}
super.endTransaction(); super.endTransaction();
} }
@ -255,11 +388,6 @@ public class MultiTileLayer extends Layer {
@Override @Override
public void draw(RenderContext context) { public void draw(RenderContext context) {
for (SubTile layer : mTiles) { 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 // Avoid work, only draw tiles that intersect with the viewport
RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize())); RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize()));
if (RectF.intersects(layerBounds, context.viewport)) 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 x;
public int y; public int y;
public SubTile(CairoImage mImage, int mX, int mY) { public Long key;
super(mImage);
x = mX; public SubTile(SubImage aImage) {
y = mY; 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();
} }
} }
} }

View File

@ -99,11 +99,18 @@ public final class RectUtils {
y + (rect.height() * scale)); y + (rect.height() * scale));
} }
/** Returns the nearest integer rect of the given rect. */
public static Rect round(RectF rect) { public static Rect round(RectF rect) {
return new Rect(Math.round(rect.left), Math.round(rect.top), return new Rect(Math.round(rect.left), Math.round(rect.top),
Math.round(rect.right), Math.round(rect.bottom)); 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) { public static IntSize getSize(Rect rect) {
return new IntSize(rect.width(), rect.height()); return new IntSize(rect.width(), rect.height());
} }

View File

@ -43,6 +43,7 @@ import org.mozilla.gecko.gfx.IntSize;
import org.mozilla.gecko.gfx.LayerController; import org.mozilla.gecko.gfx.LayerController;
import org.mozilla.gecko.gfx.TileLayer; import org.mozilla.gecko.gfx.TileLayer;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.opengl.GLES11; import android.opengl.GLES11;
import android.opengl.GLES11Ext; 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. * TODO: Repeating textures really should be their own type of layer.
*/ */
public class SingleTileLayer extends TileLayer { public class SingleTileLayer extends TileLayer {
private static final String LOGTAG = "GeckoSingleTileLayer";
public SingleTileLayer(CairoImage image) { this(false, image); } public SingleTileLayer(CairoImage image) { this(false, image); }
public SingleTileLayer(boolean repeat, CairoImage image) { public SingleTileLayer(boolean repeat, CairoImage image) {
@ -71,6 +74,11 @@ public class SingleTileLayer extends TileLayer {
if (!initialized()) if (!initialized())
return; 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()); GLES11.glBindTexture(GL10.GL_TEXTURE_2D, getTextureID());
RectF bounds; RectF bounds;
@ -79,13 +87,27 @@ public class SingleTileLayer extends TileLayer {
RectF viewport = context.viewport; RectF viewport = context.viewport;
if (repeats()) { 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()); bounds = new RectF(0.0f, 0.0f, viewport.width(), viewport.height());
int width = Math.round(viewport.width()); int width = Math.round(viewport.width());
int height = Math.round(-viewport.height()); int height = Math.round(-viewport.height());
cropRect = new int[] { 0, size.height, width, height }; cropRect = new int[] { 0, size.height, width, height };
} else { } else {
bounds = getBounds(context, new FloatSize(size)); 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, GLES11.glTexParameteriv(GL10.GL_TEXTURE_2D, GLES11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect,

View File

@ -59,14 +59,14 @@ public abstract class TileLayer extends Layer {
private final CairoImage mImage; private final CairoImage mImage;
private final boolean mRepeat; private final boolean mRepeat;
private IntSize mSize; private IntSize mSize;
private boolean mSkipTextureUpdate; private Rect mValidTextureRect;
private int[] mTextureIDs; private int[] mTextureIDs;
public TileLayer(boolean repeat, CairoImage image) { public TileLayer(boolean repeat, CairoImage image) {
mRepeat = repeat; mRepeat = repeat;
mImage = image; mImage = image;
mSize = new IntSize(0, 0); mSize = new IntSize(0, 0);
mSkipTextureUpdate = false; mValidTextureRect = new Rect();
IntSize bufferSize = mImage.getSize(); IntSize bufferSize = mImage.getSize();
mDirtyRect = new Rect(); 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) { * Tells the tile that its texture contents are invalid. This will also
mSkipTextureUpdate = skip; * 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 @Override
protected boolean performUpdates(GL10 gl, RenderContext context) { protected boolean performUpdates(GL10 gl, RenderContext context) {
super.performUpdates(gl, context); super.performUpdates(gl, context);
if (mSkipTextureUpdate)
return false;
// Reallocate the texture if the size has changed // Reallocate the texture if the size has changed
validateTexture(gl); validateTexture(gl);
@ -155,23 +160,20 @@ public abstract class TileLayer extends Layer {
if (!mImage.getSize().isPositive()) if (!mImage.getSize().isPositive())
return true; return true;
// If we haven't allocated a texture, assume the whole region is dirty // Update the dirty region
if (mTextureIDs == null) uploadDirtyRect(gl, mDirtyRect);
uploadFullTexture(gl);
else
uploadDirtyRect(gl, mDirtyRect);
mDirtyRect.setEmpty(); mDirtyRect.setEmpty();
return true; 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) { 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 we have nothing to upload, just return for now
if (dirtyRect.isEmpty()) if (dirtyRect.isEmpty())
return; return;
@ -181,6 +183,11 @@ public abstract class TileLayer extends Layer {
if (imageBuffer == null) if (imageBuffer == null)
return; 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; boolean newlyCreated = false;
if (mTextureIDs == null) { if (mTextureIDs == null) {
@ -189,15 +196,12 @@ public abstract class TileLayer extends Layer {
newlyCreated = true; newlyCreated = true;
} }
IntSize bufferSize = mImage.getSize();
Rect bufferRect = new Rect(0, 0, bufferSize.width, bufferSize.height);
int cairoFormat = mImage.getFormat(); int cairoFormat = mImage.getFormat();
CairoGLInfo glInfo = new CairoGLInfo(cairoFormat); CairoGLInfo glInfo = new CairoGLInfo(cairoFormat);
bindAndSetGLParameters(gl); bindAndSetGLParameters(gl);
if (newlyCreated || dirtyRect.contains(bufferRect)) { if (newlyCreated || dirtyRect.equals(bufferRect)) {
if (mSize.equals(bufferSize)) { if (mSize.equals(bufferSize)) {
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, mSize.height, gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, mSize.height,
0, glInfo.format, glInfo.type, imageBuffer); 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 * 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, * 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); viewBuffer.position(position);
gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top, bufferSize.width, gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, dirtyRect.top,
Math.min(bufferSize.height - dirtyRect.top, dirtyRect.height()), bufferSize.width, dirtyRect.height(),
glInfo.format, glInfo.type, viewBuffer); glInfo.format, glInfo.type, viewBuffer);
} }

View File

@ -2023,6 +2023,7 @@ Tab.prototype = {
// Is it on the top level? // Is it on the top level?
let contentDocument = aSubject; let contentDocument = aSubject;
if (contentDocument == this.browser.contentDocument) { if (contentDocument == this.browser.contentDocument) {
sendMessageToJava({ gecko: { type: "Document:Shown" } });
ViewportHandler.updateMetadata(this); ViewportHandler.updateMetadata(this);
this.documentIdForCurrentViewport = ViewportHandler.getIdForDocument(contentDocument); this.documentIdForCurrentViewport = ViewportHandler.getIdForDocument(contentDocument);
} }

View File

@ -107,6 +107,7 @@ jmethodID AndroidAddress::jGetThoroughfareMethod;
jclass AndroidGeckoSoftwareLayerClient::jGeckoSoftwareLayerClientClass = 0; jclass AndroidGeckoSoftwareLayerClient::jGeckoSoftwareLayerClientClass = 0;
jmethodID AndroidGeckoSoftwareLayerClient::jLockBufferMethod = 0; jmethodID AndroidGeckoSoftwareLayerClient::jLockBufferMethod = 0;
jmethodID AndroidGeckoSoftwareLayerClient::jUnlockBufferMethod = 0; jmethodID AndroidGeckoSoftwareLayerClient::jUnlockBufferMethod = 0;
jmethodID AndroidGeckoSoftwareLayerClient::jGetRenderOffsetMethod = 0;
jmethodID AndroidGeckoSoftwareLayerClient::jBeginDrawingMethod = 0; jmethodID AndroidGeckoSoftwareLayerClient::jBeginDrawingMethod = 0;
jmethodID AndroidGeckoSoftwareLayerClient::jEndDrawingMethod = 0; jmethodID AndroidGeckoSoftwareLayerClient::jEndDrawingMethod = 0;
jclass AndroidGeckoSurfaceView::jGeckoSurfaceViewClass = 0; jclass AndroidGeckoSurfaceView::jGeckoSurfaceViewClass = 0;
@ -323,8 +324,9 @@ AndroidGeckoSoftwareLayerClient::InitGeckoSoftwareLayerClientClass(JNIEnv *jEnv)
jLockBufferMethod = getMethod("lockBuffer", "()Ljava/nio/ByteBuffer;"); jLockBufferMethod = getMethod("lockBuffer", "()Ljava/nio/ByteBuffer;");
jUnlockBufferMethod = getMethod("unlockBuffer", "()V"); jUnlockBufferMethod = getMethod("unlockBuffer", "()V");
jBeginDrawingMethod = getMethod("beginDrawing", "(II)V"); jGetRenderOffsetMethod = getMethod("getRenderOffset", "()Landroid/graphics/Point;");
jEndDrawingMethod = getMethod("endDrawing", "(IIIILjava/lang/String;Z)V"); jBeginDrawingMethod = getMethod("beginDrawing", "(IILjava/lang/String;Z)Z");
jEndDrawingMethod = getMethod("endDrawing", "(IIII)V");
#endif #endif
} }
@ -618,21 +620,28 @@ AndroidGeckoSoftwareLayerClient::UnlockBuffer()
} }
void 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!"); NS_ASSERTION(!isNull(), "BeginDrawing() called on null software layer client!");
AndroidBridge::AutoLocalJNIFrame(1); 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 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!"); NS_ASSERTION(!isNull(), "EndDrawing() called on null software layer client!");
AndroidBridge::AutoLocalJNIFrame(1); 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);
return JNI()->CallVoidMethod(wrapped_obj, jEndDrawingMethod, aRect.x, aRect.y, aRect.width,
aRect.height, jMetadata, aHasDirectTexture);
} }
jobject jobject

View File

@ -161,8 +161,9 @@ public:
jobject LockBuffer(); jobject LockBuffer();
unsigned char *LockBufferBits(); unsigned char *LockBufferBits();
void UnlockBuffer(); void UnlockBuffer();
void BeginDrawing(int aWidth, int aHeight); void GetRenderOffset(nsIntPoint &aOffset);
void EndDrawing(const nsIntRect &aRect, const nsAString &aMetadata, bool aHasDirectTexture); bool BeginDrawing(int aWidth, int aHeight, const nsAString &aMetadata, bool aHasDirectTexture);
void EndDrawing(const nsIntRect &aRect);
private: private:
static jclass jGeckoSoftwareLayerClientClass; static jclass jGeckoSoftwareLayerClientClass;
@ -170,6 +171,7 @@ private:
static jmethodID jUnlockBufferMethod; static jmethodID jUnlockBufferMethod;
protected: protected:
static jmethodID jGetRenderOffsetMethod;
static jmethodID jBeginDrawingMethod; static jmethodID jBeginDrawingMethod;
static jmethodID jEndDrawingMethod; static jmethodID jEndDrawingMethod;
}; };

View File

@ -1195,13 +1195,22 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae)
return; return;
} }
nsAutoString metadata;
if (metadataProvider) {
metadataProvider->GetDrawMetadata(metadata);
}
AndroidGeckoSoftwareLayerClient &client = AndroidGeckoSoftwareLayerClient &client =
AndroidBridge::Bridge()->GetSoftwareLayerClient(); 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)); nsIntRect dirtyRect = ae->Rect().Intersect(nsIntRect(0, 0, gAndroidBounds.width, gAndroidBounds.height));
nsAutoString metadata;
unsigned char *bits = NULL; unsigned char *bits = NULL;
if (HasDirectTexture()) { if (HasDirectTexture()) {
if (sDirectTexture->Width() != gAndroidBounds.width || if (sDirectTexture->Width() != gAndroidBounds.width ||
@ -1220,37 +1229,32 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae)
int tileWidth = (gAndroidTileSize.width > 0) ? gAndroidTileSize.width : gAndroidBounds.width; int tileWidth = (gAndroidTileSize.width > 0) ? gAndroidTileSize.width : gAndroidBounds.width;
int tileHeight = (gAndroidTileSize.height > 0) ? gAndroidTileSize.height : gAndroidBounds.height; int tileHeight = (gAndroidTileSize.height > 0) ? gAndroidTileSize.height : gAndroidBounds.height;
bool drawSuccess = true;
int offset = 0; int offset = 0;
for (int y = 0; y < gAndroidBounds.height; y += tileHeight) { // It is assumed that the buffer has been over-allocated so that not
for (int x = 0; x < gAndroidBounds.width; x += tileWidth) { // only is the tile-size constant, but that a render-offset of anything
int width = NS_MIN(tileWidth, gAndroidBounds.width - x); // up to (but not including) the tile size could be accommodated.
int height = NS_MIN(tileHeight, gAndroidBounds.height - y); for (int y = 0; y < gAndroidBounds.height + gAndroidTileSize.height; y += tileHeight) {
for (int x = 0; x < gAndroidBounds.width + gAndroidTileSize.width; x += tileWidth) {
nsRefPtr<gfxImageSurface> targetSurface = nsRefPtr<gfxImageSurface> targetSurface =
new gfxImageSurface(bits + offset, new gfxImageSurface(bits + offset,
gfxIntSize(width, height), gfxIntSize(tileWidth, tileHeight),
width * 2, tileWidth * 2,
gfxASurface::ImageFormatRGB16_565); gfxASurface::ImageFormatRGB16_565);
offset += width * height * 2; offset += tileWidth * tileHeight * 2;
if (targetSurface->CairoStatus()) { if (targetSurface->CairoStatus()) {
ALOG("### Failed to create a valid surface from the bitmap"); ALOG("### Failed to create a valid surface from the bitmap");
drawSuccess = false;
break; break;
} else { } else {
targetSurface->SetDeviceOffset(gfxPoint(-x, -y)); targetSurface->SetDeviceOffset(gfxPoint(renderOffset.x - x,
renderOffset.y - y));
DrawTo(targetSurface, dirtyRect); DrawTo(targetSurface, dirtyRect);
} }
} }
} }
// Don't fill in the draw metadata on an unsuccessful draw
if (drawSuccess && metadataProvider) {
metadataProvider->GetDrawMetadata(metadata);
}
} }
if (HasDirectTexture()) { if (HasDirectTexture()) {
@ -1259,7 +1263,7 @@ nsWindow::OnDraw(AndroidGeckoEvent *ae)
client.UnlockBuffer(); client.UnlockBuffer();
} }
client.EndDrawing(dirtyRect, metadata, HasDirectTexture()); client.EndDrawing(dirtyRect);
return; return;
#endif #endif