mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
c1e993df15
The buffer in MultiTileLayer is invalidated each time the origin or resolution changes. MultiTileLayer was using the last updated origin/resolution instead of the last set, so if Gecko was animating and locked the layer for long enough, the buffer would be incorrectly invalidated due to the origin/resolution not being updated.
529 lines
20 KiB
Java
529 lines
20 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Android code.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2011-2012
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Chris Lord <chrislord.net@gmail.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
package org.mozilla.gecko.gfx;
|
|
|
|
import org.mozilla.gecko.FloatUtils;
|
|
import org.mozilla.gecko.gfx.CairoImage;
|
|
import org.mozilla.gecko.gfx.IntSize;
|
|
import org.mozilla.gecko.gfx.SingleTileLayer;
|
|
import android.graphics.Point;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.graphics.Region;
|
|
import android.util.Log;
|
|
import java.lang.Long;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.ListIterator;
|
|
import javax.microedition.khronos.opengles.GL10;
|
|
|
|
/**
|
|
* Encapsulates the logic needed to draw a layer made of multiple tiles.
|
|
*
|
|
* TODO: Support repeating.
|
|
*/
|
|
public class MultiTileLayer extends Layer {
|
|
private static final String LOGTAG = "GeckoMultiTileLayer";
|
|
|
|
private final CairoImage mImage;
|
|
private final IntSize mTileSize;
|
|
private IntSize mBufferSize;
|
|
private Region mDirtyRegion;
|
|
private Region mValidRegion;
|
|
private Point mRenderOffset;
|
|
private final LinkedList<SubTile> mTiles;
|
|
private final HashMap<Long, SubTile> mPositionHash;
|
|
|
|
// Copies of the last set origin/resolution, to decide when to invalidate
|
|
// the buffer
|
|
private Point mOrigin;
|
|
private float mResolution;
|
|
|
|
public MultiTileLayer(CairoImage image, IntSize tileSize) {
|
|
super();
|
|
|
|
mImage = image;
|
|
mTileSize = tileSize;
|
|
mBufferSize = new IntSize(0, 0);
|
|
mDirtyRegion = new Region();
|
|
mValidRegion = new Region();
|
|
mRenderOffset = new Point();
|
|
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) {
|
|
if (!inTransaction()) {
|
|
throw new RuntimeException("invalidate() is only valid inside a transaction");
|
|
}
|
|
|
|
mDirtyRegion.union(dirtyRect);
|
|
mValidRegion.union(dirtyRect);
|
|
}
|
|
|
|
/**
|
|
* Invalidates the backing buffer. Data will not be uploaded from an invalid
|
|
* backing buffer. This method is only valid inside a transaction.
|
|
*/
|
|
protected void invalidateBuffer() {
|
|
if (!inTransaction()) {
|
|
throw new RuntimeException("invalidateBuffer() is only valid inside a transaction");
|
|
}
|
|
|
|
mDirtyRegion.setEmpty();
|
|
mValidRegion.setEmpty();
|
|
}
|
|
|
|
@Override
|
|
public IntSize getSize() {
|
|
return mImage.getSize();
|
|
}
|
|
|
|
/**
|
|
* Makes sure there are enough tiles to accommodate the buffer image.
|
|
*/
|
|
private void validateTiles() {
|
|
IntSize size = getSize();
|
|
|
|
if (size.equals(mBufferSize))
|
|
return;
|
|
|
|
mBufferSize = size;
|
|
|
|
// Shrink/grow tile pool
|
|
int nTiles = (Math.round(size.width / (float)mTileSize.width) + 1) *
|
|
(Math.round(size.height / (float)mTileSize.height) + 1);
|
|
if (mTiles.size() < nTiles) {
|
|
Log.i(LOGTAG, "Tile pool growing from " + mTiles.size() + " to " + nTiles);
|
|
|
|
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);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// A buffer size probably means a layout change, so invalidate all tiles.
|
|
invalidateTiles();
|
|
}
|
|
|
|
/**
|
|
* Returns a Long representing the given Point. Used for hashing.
|
|
*/
|
|
private Long longFromPoint(Point point) {
|
|
// Assign 32 bits for each dimension of the point.
|
|
return new Long((((long)point.x) << 32) | point.y);
|
|
}
|
|
|
|
private Point getOffsetOrigin() {
|
|
Point origin = new Point(getOrigin());
|
|
origin.offset(-mRenderOffset.x, -mRenderOffset.y);
|
|
return origin;
|
|
}
|
|
|
|
/**
|
|
* 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 = getOffsetOrigin();
|
|
Region validRegion = new Region(tile.getValidTextureArea());
|
|
validRegion.translate(tileOrigin.x - origin.x, tileOrigin.y - origin.y);
|
|
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.isEmpty() && !validRegion.isComplex()) {
|
|
validRegion.translate(origin.x - tileOrigin.x, origin.y - tileOrigin.y);
|
|
tile.getValidTextureArea().set(validRegion.getBounds());
|
|
}
|
|
} 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
|
|
protected boolean performUpdates(GL10 gl, RenderContext context) {
|
|
super.performUpdates(gl, context);
|
|
|
|
validateTiles();
|
|
|
|
// Bail out early if we have nothing to do.
|
|
if (mDirtyRegion.isEmpty() || mTiles.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
// Check that we're capable of updating from this origin.
|
|
Point origin = getOffsetOrigin();
|
|
if ((origin.x % mTileSize.width) != 0 || (origin.y % mTileSize.height) != 0) {
|
|
Log.e(LOGTAG, "MultiTileLayer doesn't support non tile-aligned buffers! (" +
|
|
origin.x + ", " + origin.y + ")");
|
|
return true;
|
|
}
|
|
|
|
// 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<SubTile> invalidTiles = new LinkedList<SubTile>();
|
|
for (ListIterator<SubTile> i = mTiles.listIterator(); i.hasNext();) {
|
|
SubTile tile = i.next();
|
|
|
|
if (tile.key == null) {
|
|
continue;
|
|
}
|
|
|
|
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 ((!opRegion.op(tilespaceTileBounds, mValidRegion, Region.Op.INTERSECT) &&
|
|
!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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
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);
|
|
|
|
// Reuse the tile (i.e. keep the texture data and metrics)
|
|
// only if the resolution matches
|
|
reusedTile = FloatUtils.fuzzyEquals(tile.getResolution(), getResolution());
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the update region from the dirty region
|
|
mDirtyRegion.op(updateRegion, Region.Op.XOR);
|
|
|
|
return mDirtyRegion.isEmpty();
|
|
}
|
|
|
|
@Override
|
|
public void beginTransaction(LayerView aView) {
|
|
super.beginTransaction(aView);
|
|
|
|
for (SubTile layer : mTiles) {
|
|
layer.beginTransaction(aView);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void endTransaction() {
|
|
for (SubTile layer : mTiles) {
|
|
layer.endTransaction();
|
|
}
|
|
|
|
super.endTransaction();
|
|
}
|
|
|
|
@Override
|
|
public void draw(RenderContext context) {
|
|
for (SubTile layer : mTiles) {
|
|
// Skip invalid tiles
|
|
if (layer.key == null) {
|
|
continue;
|
|
}
|
|
|
|
// Avoid work, only draw tiles that intersect with the viewport
|
|
RectF layerBounds = layer.getBounds(context, new FloatSize(layer.getSize()));
|
|
if (RectF.intersects(layerBounds, context.viewport)) {
|
|
layer.draw(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setOrigin(Point origin) {
|
|
if (mOrigin == null || !origin.equals(mOrigin)) {
|
|
mOrigin = origin;
|
|
super.setOrigin(origin);
|
|
invalidateBuffer();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setResolution(float resolution) {
|
|
if (!FloatUtils.fuzzyEquals(resolution, mResolution)) {
|
|
mResolution = resolution;
|
|
super.setResolution(resolution);
|
|
invalidateBuffer();
|
|
}
|
|
}
|
|
|
|
public void setRenderOffset(Point offset) {
|
|
mRenderOffset.set(offset.x, offset.y);
|
|
}
|
|
|
|
/**
|
|
* Invalidates all sub-tiles. This should be called if the source backing
|
|
* this layer has changed. This method is only valid inside a transaction.
|
|
*/
|
|
protected void invalidateTiles() {
|
|
if (!inTransaction()) {
|
|
throw new RuntimeException("invalidateTiles() is only valid inside a transaction");
|
|
}
|
|
|
|
for (SubTile tile : mTiles) {
|
|
// Remove tile from position hash and mark it as invalid
|
|
if (tile.key != null) {
|
|
mPositionHash.remove(tile.key);
|
|
tile.key = null;
|
|
}
|
|
tile.invalidateTexture();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A SingleTileLayer extension with fields for relevant tile data that
|
|
* MultiTileLayer requires.
|
|
*/
|
|
private static class SubTile extends SingleTileLayer {
|
|
public int x;
|
|
public int y;
|
|
|
|
public Long key;
|
|
|
|
public SubTile(SubImage aImage) {
|
|
super(aImage);
|
|
|
|
aImage.tile = this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A CairoImage implementation that returns a tile from a parent CairoImage.
|
|
* This assumes that the parent image has a size that is a multiple of the
|
|
* tile size.
|
|
*/
|
|
private static class SubImage extends CairoImage {
|
|
public SubTile tile;
|
|
|
|
private IntSize mTileSize;
|
|
private CairoImage mImage;
|
|
|
|
public SubImage(CairoImage image, IntSize tileSize) {
|
|
mTileSize = tileSize;
|
|
mImage = image;
|
|
}
|
|
|
|
@Override
|
|
public ByteBuffer getBuffer() {
|
|
// Create a ByteBuffer that shares the data of the original
|
|
// buffer, but is positioned and limited so that only the
|
|
// tile data is accessible.
|
|
IntSize bufferSize = mImage.getSize();
|
|
int bpp = CairoUtils.bitsPerPixelForCairoFormat(getFormat()) / 8;
|
|
int index = (tile.y * (bufferSize.width / mTileSize.width + 1)) + tile.x;
|
|
|
|
ByteBuffer buffer = mImage.getBuffer().slice();
|
|
|
|
try {
|
|
buffer.position(index * mTileSize.getArea() * bpp);
|
|
buffer = buffer.slice();
|
|
buffer.limit(mTileSize.getArea() * bpp);
|
|
} catch (IllegalArgumentException e) {
|
|
Log.e(LOGTAG, "Tile image-data out of bounds! Tile: (" +
|
|
tile.x + ", " + tile.y + "), image (" + bufferSize + ")");
|
|
return null;
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
@Override
|
|
public IntSize getSize() {
|
|
return mTileSize;
|
|
}
|
|
|
|
@Override
|
|
public int getFormat() {
|
|
return mImage.getFormat();
|
|
}
|
|
}
|
|
}
|
|
|