michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.PrefsHelper; michael@0: import org.mozilla.gecko.util.FloatUtils; michael@0: michael@0: import org.json.JSONArray; michael@0: michael@0: import android.graphics.PointF; michael@0: import android.graphics.RectF; michael@0: import android.util.FloatMath; michael@0: import android.util.Log; michael@0: michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: michael@0: final class DisplayPortCalculator { michael@0: private static final String LOGTAG = "GeckoDisplayPort"; michael@0: private static final PointF ZERO_VELOCITY = new PointF(0, 0); michael@0: michael@0: // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h michael@0: private static final int TILE_SIZE = 256; michael@0: michael@0: private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy"; michael@0: private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier"; michael@0: private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x"; michael@0: private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y"; michael@0: private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier"; michael@0: private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold"; michael@0: private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer"; michael@0: private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base"; michael@0: private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base"; michael@0: private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr"; michael@0: private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr"; michael@0: private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold"; michael@0: michael@0: private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null); michael@0: michael@0: static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity)); michael@0: } michael@0: michael@0: static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: if (displayPort == null) { michael@0: return true; michael@0: } michael@0: return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort); michael@0: } michael@0: michael@0: static boolean drawTimeUpdate(long millis, int pixels) { michael@0: return sStrategy.drawTimeUpdate(millis, pixels); michael@0: } michael@0: michael@0: static void resetPageState() { michael@0: sStrategy.resetPageState(); michael@0: } michael@0: michael@0: static void initPrefs() { michael@0: final String[] prefs = { PREF_DISPLAYPORT_STRATEGY, michael@0: PREF_DISPLAYPORT_FM_MULTIPLIER, michael@0: PREF_DISPLAYPORT_FM_DANGER_X, michael@0: PREF_DISPLAYPORT_FM_DANGER_Y, michael@0: PREF_DISPLAYPORT_VB_MULTIPLIER, michael@0: PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, michael@0: PREF_DISPLAYPORT_VB_REVERSE_BUFFER, michael@0: PREF_DISPLAYPORT_VB_DANGER_X_BASE, michael@0: PREF_DISPLAYPORT_VB_DANGER_Y_BASE, michael@0: PREF_DISPLAYPORT_VB_DANGER_X_INCR, michael@0: PREF_DISPLAYPORT_VB_DANGER_Y_INCR, michael@0: PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD }; michael@0: michael@0: PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() { michael@0: private Map mValues = new HashMap(); michael@0: michael@0: @Override public void prefValue(String pref, int value) { michael@0: mValues.put(pref, value); michael@0: } michael@0: michael@0: @Override public void finish() { michael@0: setStrategy(mValues); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Set the active strategy to use. michael@0: * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the michael@0: * mapping between ints and strategies. michael@0: */ michael@0: static boolean setStrategy(Map prefs) { michael@0: Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY); michael@0: if (strategy == null) { michael@0: return false; michael@0: } michael@0: michael@0: switch (strategy) { michael@0: case 0: michael@0: sStrategy = new FixedMarginStrategy(prefs); michael@0: break; michael@0: case 1: michael@0: sStrategy = new VelocityBiasStrategy(prefs); michael@0: break; michael@0: case 2: michael@0: sStrategy = new DynamicResolutionStrategy(prefs); michael@0: break; michael@0: case 3: michael@0: sStrategy = new NoMarginStrategy(prefs); michael@0: break; michael@0: case 4: michael@0: sStrategy = new PredictionBiasStrategy(prefs); michael@0: break; michael@0: default: michael@0: Log.e(LOGTAG, "Invalid strategy index specified"); michael@0: return false; michael@0: } michael@0: Log.i(LOGTAG, "Set strategy " + sStrategy.toString()); michael@0: return true; michael@0: } michael@0: michael@0: private static float getFloatPref(Map prefs, String prefName, int defaultValue) { michael@0: Integer value = (prefs == null ? null : prefs.get(prefName)); michael@0: return (float)(value == null || value < 0 ? defaultValue : value) / 1000f; michael@0: } michael@0: michael@0: private static abstract class DisplayPortStrategy { michael@0: /** Calculates a displayport given a viewport and panning velocity. */ michael@0: public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity); michael@0: /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */ michael@0: public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort); michael@0: /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */ michael@0: public boolean drawTimeUpdate(long millis, int pixels) { return false; } michael@0: /** Reset any page-specific state stored, as the page being displayed has changed. */ michael@0: public void resetPageState() {} michael@0: } michael@0: michael@0: /** michael@0: * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the michael@0: * given metrics object. The area in the returned FloatSize may be less than width*height if the page is michael@0: * small, but it will never be larger than width*height. michael@0: * Note that this process may change the relative aspect ratio of the given dimensions. michael@0: */ michael@0: private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) { michael@0: // figure out how much of the desired buffer amount we can actually use on the horizontal axis michael@0: float usableWidth = Math.min(width, metrics.getPageWidth()); michael@0: // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and michael@0: // use it on the vertical axis michael@0: float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth); michael@0: float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight()); michael@0: if (usableHeight < height && usableWidth == width) { michael@0: // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal michael@0: float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight); michael@0: usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth()); michael@0: } michael@0: return new FloatSize(usableWidth, usableHeight); michael@0: } michael@0: michael@0: /** michael@0: * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis michael@0: * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then michael@0: * clamped to page bounds and returned. michael@0: */ michael@0: private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) { michael@0: // calculate the danger zone amounts in pixels michael@0: float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier; michael@0: float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier; michael@0: rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY); michael@0: // clamp to page bounds michael@0: return clampToPageBounds(rect, metrics); michael@0: } michael@0: michael@0: /** michael@0: * Expand the given margins such that when they are applied on the viewport, the resulting rect michael@0: * does not have any partial tiles, except when it is clipped by the page bounds. This assumes michael@0: * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be michael@0: * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)). michael@0: */ michael@0: private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) { michael@0: float left = metrics.viewportRectLeft - margins.left; michael@0: float top = metrics.viewportRectTop - margins.top; michael@0: float right = metrics.viewportRectRight + margins.right; michael@0: float bottom = metrics.viewportRectBottom + margins.bottom; michael@0: left = Math.max(metrics.pageRectLeft, TILE_SIZE * FloatMath.floor(left / TILE_SIZE)); michael@0: top = Math.max(metrics.pageRectTop, TILE_SIZE * FloatMath.floor(top / TILE_SIZE)); michael@0: right = Math.min(metrics.pageRectRight, TILE_SIZE * FloatMath.ceil(right / TILE_SIZE)); michael@0: bottom = Math.min(metrics.pageRectBottom, TILE_SIZE * FloatMath.ceil(bottom / TILE_SIZE)); michael@0: return new DisplayPortMetrics(left, top, right, bottom, zoom); michael@0: } michael@0: michael@0: /** michael@0: * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect michael@0: * does not exceed the page bounds. This code will maintain the total margin amount for a given axis; michael@0: * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to michael@0: * metrics.getPageWidth(); and the same for the y axis. michael@0: */ michael@0: private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) { michael@0: // check how much we're overflowing in each direction. note that at most one of leftOverflow michael@0: // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow michael@0: // can be greater than zero, because of the assumption described in the method javadoc. michael@0: float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left); michael@0: float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight; michael@0: float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top); michael@0: float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom; michael@0: michael@0: // if the margins overflow the page bounds, shift them to other side on the same axis michael@0: if (leftOverflow > 0) { michael@0: margins.left -= leftOverflow; michael@0: margins.right += leftOverflow; michael@0: } else if (rightOverflow > 0) { michael@0: margins.right -= rightOverflow; michael@0: margins.left += rightOverflow; michael@0: } michael@0: if (topOverflow > 0) { michael@0: margins.top -= topOverflow; michael@0: margins.bottom += topOverflow; michael@0: } else if (bottomOverflow > 0) { michael@0: margins.bottom -= bottomOverflow; michael@0: margins.top += bottomOverflow; michael@0: } michael@0: return margins; michael@0: } michael@0: michael@0: /** michael@0: * Clamp the given rect to the page bounds and return it. michael@0: */ michael@0: private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) { michael@0: if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop; michael@0: if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft; michael@0: if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight; michael@0: if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom; michael@0: return rect; michael@0: } michael@0: michael@0: /** michael@0: * This class implements the variation where we basically don't bother with a a display port. michael@0: */ michael@0: private static class NoMarginStrategy extends DisplayPortStrategy { michael@0: NoMarginStrategy(Map prefs) { michael@0: // no prefs in this strategy michael@0: } michael@0: michael@0: @Override michael@0: public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: return new DisplayPortMetrics(metrics.viewportRectLeft, michael@0: metrics.viewportRectTop, michael@0: metrics.viewportRectRight, michael@0: metrics.viewportRectBottom, michael@0: metrics.zoomFactor); michael@0: } michael@0: michael@0: @Override michael@0: public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "NoMarginStrategy"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This class implements the variation where we use a fixed-size margin on the display port. michael@0: * The margin is always 300 pixels in all directions, except when we are (a) approaching a page michael@0: * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain michael@0: * the area of the display port by (a) shifting the buffer to the other side on the same axis, michael@0: * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on michael@0: * one axis. michael@0: */ michael@0: private static class FixedMarginStrategy extends DisplayPortStrategy { michael@0: // The length of each axis of the display port will be the corresponding view length michael@0: // multiplied by this factor. michael@0: private final float SIZE_MULTIPLIER; michael@0: michael@0: // If the visible rect is within the danger zone (measured as a fraction of the view size michael@0: // from the edge of the displayport) we start redrawing to minimize checkerboarding. michael@0: private final float DANGER_ZONE_X_MULTIPLIER; michael@0: private final float DANGER_ZONE_Y_MULTIPLIER; michael@0: michael@0: FixedMarginStrategy(Map prefs) { michael@0: SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000); michael@0: DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100); michael@0: DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200); michael@0: } michael@0: michael@0: @Override michael@0: public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; michael@0: float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; michael@0: michael@0: // we need to avoid having a display port that is larger than the page, or we will end up michael@0: // painting things outside the page bounds (bug 729169). we simultaneously need to make michael@0: // the display port as large as possible so that we redraw less. reshape the display michael@0: // port dimensions to accomplish this. michael@0: FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); michael@0: float horizontalBuffer = usableSize.width - metrics.getWidth(); michael@0: float verticalBuffer = usableSize.height - metrics.getHeight(); michael@0: michael@0: // and now calculate the display port margins based on how much buffer we've decided to use and michael@0: // the page bounds, ensuring we use all of the available buffer amounts on one side or the other michael@0: // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is michael@0: // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer michael@0: // is split). michael@0: RectF margins = new RectF(); michael@0: margins.left = horizontalBuffer / 2.0f; michael@0: margins.right = horizontalBuffer - margins.left; michael@0: margins.top = verticalBuffer / 2.0f; michael@0: margins.bottom = verticalBuffer - margins.top; michael@0: margins = shiftMarginsForPageBounds(margins, metrics); michael@0: michael@0: return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); michael@0: } michael@0: michael@0: @Override michael@0: public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: // Increase the size of the viewport based on the danger zone multiplier (and clamp to page michael@0: // boundaries), and intersect it with the current displayport to determine whether we're michael@0: // close to checkerboarding. michael@0: RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics); michael@0: return !displayPort.contains(adjustedViewport); michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This class implements the variation with a small fixed-size margin with velocity bias. michael@0: * In this variation, the default margins are pretty small relative to the view size, but michael@0: * they are affected by the panning velocity. Specifically, if we are panning on one axis, michael@0: * we remove the margins on the other axis because we are likely axis-locked. Also once michael@0: * we are panning in one direction above a certain threshold velocity, we shift the buffer michael@0: * so that it is almost entirely in the direction of the pan, with a little bit in the michael@0: * reverse direction. michael@0: */ michael@0: private static class VelocityBiasStrategy extends DisplayPortStrategy { michael@0: // The length of each axis of the display port will be the corresponding view length michael@0: // multiplied by this factor. michael@0: private final float SIZE_MULTIPLIER; michael@0: // The velocity above which we apply the velocity bias michael@0: private final float VELOCITY_THRESHOLD; michael@0: // How much of the buffer to keep in the reverse direction of the velocity michael@0: private final float REVERSE_BUFFER; michael@0: // If the visible rect is within the danger zone we start redrawing to minimize michael@0: // checkerboarding. the danger zone amount is a linear function of the form: michael@0: // viewportsize * (base + velocity * incr) michael@0: // where base and incr are configurable values. michael@0: private final float DANGER_ZONE_BASE_X_MULTIPLIER; michael@0: private final float DANGER_ZONE_BASE_Y_MULTIPLIER; michael@0: private final float DANGER_ZONE_INCR_X_MULTIPLIER; michael@0: private final float DANGER_ZONE_INCR_Y_MULTIPLIER; michael@0: michael@0: VelocityBiasStrategy(Map prefs) { michael@0: SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000); michael@0: VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32); michael@0: REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200); michael@0: DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000); michael@0: DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000); michael@0: DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0); michael@0: DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0); michael@0: } michael@0: michael@0: /** michael@0: * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values. michael@0: * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER michael@0: * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the michael@0: * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction michael@0: * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the michael@0: * two margins on that axis. michael@0: */ michael@0: private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) { michael@0: RectF margins = new RectF(); michael@0: michael@0: if (velocity.x > VELOCITY_THRESHOLD) { michael@0: margins.left = xAmount * REVERSE_BUFFER; michael@0: } else if (velocity.x < -VELOCITY_THRESHOLD) { michael@0: margins.left = xAmount * (1.0f - REVERSE_BUFFER); michael@0: } else { michael@0: margins.left = xAmount / 2.0f; michael@0: } michael@0: margins.right = xAmount - margins.left; michael@0: michael@0: if (velocity.y > VELOCITY_THRESHOLD) { michael@0: margins.top = yAmount * REVERSE_BUFFER; michael@0: } else if (velocity.y < -VELOCITY_THRESHOLD) { michael@0: margins.top = yAmount * (1.0f - REVERSE_BUFFER); michael@0: } else { michael@0: margins.top = yAmount / 2.0f; michael@0: } michael@0: margins.bottom = yAmount - margins.top; michael@0: michael@0: return margins; michael@0: } michael@0: michael@0: @Override michael@0: public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; michael@0: float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; michael@0: michael@0: // but if we're panning on one axis, set the margins for the other axis to zero since we are likely michael@0: // axis locked and won't be displaying that extra area. michael@0: if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) { michael@0: displayPortHeight = metrics.getHeight(); michael@0: } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) { michael@0: displayPortWidth = metrics.getWidth(); michael@0: } michael@0: michael@0: // we need to avoid having a display port that is larger than the page, or we will end up michael@0: // painting things outside the page bounds (bug 729169). michael@0: displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth()); michael@0: displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight()); michael@0: float horizontalBuffer = displayPortWidth - metrics.getWidth(); michael@0: float verticalBuffer = displayPortHeight - metrics.getHeight(); michael@0: michael@0: // split the buffer amounts into margins based on velocity, and shift it to michael@0: // take into account the page bounds michael@0: RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity); michael@0: margins = shiftMarginsForPageBounds(margins, metrics); michael@0: michael@0: return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); michael@0: } michael@0: michael@0: @Override michael@0: public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: // calculate the danger zone amounts based on the prefs michael@0: float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER)); michael@0: float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER)); michael@0: // clamp it such that when added to the viewport, they don't exceed page size. michael@0: // this is a prerequisite to calling shiftMarginsForPageBounds as we do below. michael@0: dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth()); michael@0: dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight()); michael@0: michael@0: // split the danger zone into margins based on velocity, and ensure it doesn't exceed michael@0: // page bounds. michael@0: RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity); michael@0: dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics); michael@0: michael@0: // we're about to checkerboard if the current viewport area + the danger zone margins michael@0: // fall out of the current displayport anywhere. michael@0: RectF adjustedViewport = new RectF( michael@0: metrics.viewportRectLeft - dangerMargins.left, michael@0: metrics.viewportRectTop - dangerMargins.top, michael@0: metrics.viewportRectRight + dangerMargins.right, michael@0: metrics.viewportRectBottom + dangerMargins.bottom); michael@0: return !displayPort.contains(adjustedViewport); michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER michael@0: + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER michael@0: + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This class implements the variation where we draw more of the page at low resolution while panning. michael@0: * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw michael@0: * resolution to compensate. This results in the same device-pixel area drawn; the compositor then michael@0: * scales this up to the viewport zoom level. This results in a large area of the page drawn but it michael@0: * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding, michael@0: * where we draw less but never even show it on the screen. michael@0: */ michael@0: private static class DynamicResolutionStrategy extends DisplayPortStrategy { michael@0: // The length of each axis of the display port will be the corresponding view length michael@0: // multiplied by this factor. michael@0: private static final float SIZE_MULTIPLIER = 1.5f; michael@0: michael@0: // The velocity above which we start zooming out the display port to keep up michael@0: // with the panning. michael@0: private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f; michael@0: michael@0: // How much we increase the display port based on velocity. Assuming no friction and michael@0: // splitting (see below), this should be be the number of frames (@60fps) between us michael@0: // calculating the display port and the draw of the *next* display port getting composited michael@0: // and displayed on the screen. This is because the timeline looks like this: michael@0: // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan ! michael@0: // Gecko: \-> draw -> composite / \-> draw -> composite / michael@0: // The display port calculated on the first "pan" gets composited to the screen at the michael@0: // first exclamation mark, and remains on the screen until the second exclamation mark. michael@0: // In order to avoid checkerboarding, that display port must be able to contain all of michael@0: // the panning until the second exclamation mark, which encompasses two entire draw/composite michael@0: // cycles. michael@0: // If we take into account friction, our velocity multiplier should be reduced as the michael@0: // amount of pan will decrease each time. If we take into account display port splitting, michael@0: // it should be increased as the splitting means some of the display port will be used to michael@0: // draw in the opposite direction of the velocity. For now I'm assuming these two cancel michael@0: // each other out. michael@0: private static final float VELOCITY_MULTIPLIER = 60.0f; michael@0: michael@0: // The following constants adjust how biased the display port is in the direction of panning. michael@0: // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the michael@0: // display port "buffer" area, otherwise we use the slow split factor. This is based on the michael@0: // assumption that if the user is panning fast, they are less likely to reverse directions michael@0: // and go backwards, so we should spend more of our display port buffer in the direction of michael@0: // panning. michael@0: private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f; michael@0: private static final float FAST_SPLIT_FACTOR = 0.95f; michael@0: private static final float SLOW_SPLIT_FACTOR = 0.8f; michael@0: michael@0: // The following constants are used for viewport prediction; we use them to estimate where michael@0: // the viewport will be soon and whether or not we should trigger a draw right now. "soon" michael@0: // in the previous sentence really refers to the amount of time it would take to draw and michael@0: // composite from the point at which we do the calculation, and that is not really a known michael@0: // quantity. The velocity multiplier is how much we multiply the velocity by; it has the michael@0: // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account michael@0: // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the michael@0: // viewport size that we use as an extra "danger zone" around the viewport; if this danger michael@0: // zone falls outside the display port then we are approaching the point at which we will michael@0: // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is michael@0: // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the michael@0: // danger zone, and thus will be constantly drawing. michael@0: private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f; michael@0: private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f) michael@0: michael@0: DynamicResolutionStrategy(Map prefs) { michael@0: // ignore prefs for now michael@0: } michael@0: michael@0: @Override michael@0: public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; michael@0: float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; michael@0: michael@0: // for resolution calculation purposes, we need to know what the adjusted display port dimensions michael@0: // would be if we had zero velocity, so calculate that here before we increase the display port michael@0: // based on velocity. michael@0: FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); michael@0: michael@0: // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their michael@0: // relative aspect ratio. michael@0: if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) { michael@0: float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth, michael@0: Math.abs(velocity.y) / displayPortHeight); michael@0: velocityFactor *= VELOCITY_MULTIPLIER; michael@0: michael@0: displayPortWidth += (displayPortWidth * velocityFactor); michael@0: displayPortHeight += (displayPortHeight * velocityFactor); michael@0: } michael@0: michael@0: // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels) michael@0: // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied michael@0: // by metrics.zoomFactor michael@0: michael@0: // we need to avoid having a display port that is larger than the page, or we will end up michael@0: // painting things outside the page bounds (bug 729169). we simultaneously need to make michael@0: // the display port as large as possible so that we redraw less. reshape the display michael@0: // port dimensions to accomplish this. this may change the aspect ratio of the display port, michael@0: // but we are assuming that this is desirable because the advantages from pre-drawing will michael@0: // outweigh the disadvantages from any buffer reallocations that might occur. michael@0: FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); michael@0: float horizontalBuffer = usableSize.width - metrics.getWidth(); michael@0: float verticalBuffer = usableSize.height - metrics.getHeight(); michael@0: michael@0: // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have. michael@0: // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case michael@0: // the user scrolls there. we now need to split the buffer area on each axis so that we know michael@0: // what the exact margins on each side will be. first we split the buffer amount based on the direction michael@0: // we're moving, so that we have a larger buffer in the direction of travel. michael@0: RectF margins = new RectF(); michael@0: margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x); michael@0: margins.right = horizontalBuffer - margins.left; michael@0: margins.top = splitBufferByVelocity(verticalBuffer, velocity.y); michael@0: margins.bottom = verticalBuffer - margins.top; michael@0: michael@0: // then, we account for running into the page bounds - so that if we hit the top of the page, we need michael@0: // to drop the top margin and move that amount to the bottom margin. michael@0: margins = shiftMarginsForPageBounds(margins, metrics); michael@0: michael@0: // finally, we calculate the resolution we want to render the display port area at. We do this michael@0: // so that as we expand the display port area (because of velocity), we reduce the resolution of michael@0: // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate michael@0: // the reduction in resolution by comparing the display port size with and without the velocity michael@0: // changes applied. michael@0: // this effectively means that as we pan faster and faster, the display port grows, but we paint michael@0: // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing michael@0: // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone. michael@0: // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we michael@0: // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor. michael@0: float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height); michael@0: float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor); michael@0: michael@0: DisplayPortMetrics dpMetrics = new DisplayPortMetrics( michael@0: metrics.viewportRectLeft - margins.left, michael@0: metrics.viewportRectTop - margins.top, michael@0: metrics.viewportRectRight + margins.right, michael@0: metrics.viewportRectBottom + margins.bottom, michael@0: displayResolution); michael@0: return dpMetrics; michael@0: } michael@0: michael@0: /** michael@0: * Split the given buffer amount into two based on the velocity. michael@0: * Given an amount of total usable buffer on an axis, this will michael@0: * return the amount that should be used on the left/top side of michael@0: * the axis (the side which a negative velocity vector corresponds michael@0: * to). michael@0: */ michael@0: private float splitBufferByVelocity(float amount, float velocity) { michael@0: // if no velocity, so split evenly michael@0: if (FloatUtils.fuzzyEquals(velocity, 0)) { michael@0: return amount / 2.0f; michael@0: } michael@0: // if we're moving quickly, assign more of the amount in that direction michael@0: // since is is less likely that we will reverse direction immediately michael@0: if (velocity < -VELOCITY_FAST_THRESHOLD) { michael@0: return amount * FAST_SPLIT_FACTOR; michael@0: } michael@0: if (velocity > VELOCITY_FAST_THRESHOLD) { michael@0: return amount * (1.0f - FAST_SPLIT_FACTOR); michael@0: } michael@0: // if we're moving slowly, then assign less of the amount in that direction michael@0: if (velocity < 0) { michael@0: return amount * SLOW_SPLIT_FACTOR; michael@0: } else { michael@0: return amount * (1.0f - SLOW_SPLIT_FACTOR); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: // Expand the viewport based on our velocity (and clamp it to page boundaries). michael@0: // Then intersect it with the last-requested displayport to determine whether we're michael@0: // close to checkerboarding. michael@0: michael@0: RectF predictedViewport = metrics.getViewport(); michael@0: michael@0: // first we expand the viewport in the direction we're moving based on some michael@0: // multiple of the current velocity. michael@0: if (velocity.length() > 0) { michael@0: if (velocity.x < 0) { michael@0: predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; michael@0: } else if (velocity.x > 0) { michael@0: predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; michael@0: } michael@0: michael@0: if (velocity.y < 0) { michael@0: predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; michael@0: } else if (velocity.y > 0) { michael@0: predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; michael@0: } michael@0: } michael@0: michael@0: // then we expand the viewport evenly in all directions just to have an extra michael@0: // safety zone. this also clamps it to page bounds. michael@0: predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics); michael@0: return !displayPort.contains(predictedViewport); michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "DynamicResolutionStrategy"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This class implements the variation where we use the draw time to predict where we will be when michael@0: * a draw completes, and draw that instead of where we are now. In this variation, when our panning michael@0: * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can michael@0: * pan in any direction without encountering checkerboarding. michael@0: * Once the user is panning, we modify the displayport to encompass an area range of where we think michael@0: * the user will be when the draw completes. This heuristic relies on both the estimated draw time michael@0: * the panning velocity; unexpected changes in either of these values will cause the heuristic to michael@0: * fail and show checkerboard. michael@0: */ michael@0: private static class PredictionBiasStrategy extends DisplayPortStrategy { michael@0: private static float VELOCITY_THRESHOLD; michael@0: michael@0: private int mPixelArea; // area of the viewport, used in draw time calculations michael@0: private int mMinFramesToDraw; // minimum number of frames we take to draw michael@0: private int mMaxFramesToDraw; // maximum number of frames we take to draw michael@0: michael@0: PredictionBiasStrategy(Map prefs) { michael@0: VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16); michael@0: resetPageState(); michael@0: } michael@0: michael@0: @Override michael@0: public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { michael@0: float width = metrics.getWidth(); michael@0: float height = metrics.getHeight(); michael@0: mPixelArea = (int)(width * height); michael@0: michael@0: if (velocity.length() < VELOCITY_THRESHOLD) { michael@0: // if we're going slow, expand the displayport to 9x viewport size michael@0: RectF margins = new RectF(width, height, width, height); michael@0: return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); michael@0: } michael@0: michael@0: // figure out how far we expect to be michael@0: float minDx = velocity.x * mMinFramesToDraw; michael@0: float minDy = velocity.y * mMinFramesToDraw; michael@0: float maxDx = velocity.x * mMaxFramesToDraw; michael@0: float maxDy = velocity.y * mMaxFramesToDraw; michael@0: michael@0: // figure out how many pixels we will be drawing when we draw the above-calculated range. michael@0: // this will be larger than the viewport area. michael@0: float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy)); michael@0: // adjust how far we will get because of the time spent drawing all these extra pixels. this michael@0: // will again increase the number of pixels drawn so really we could keep iterating this over michael@0: // and over, but once seems enough for now. michael@0: maxDx = maxDx * pixelsToDraw / mPixelArea; michael@0: maxDy = maxDy * pixelsToDraw / mPixelArea; michael@0: michael@0: // and finally generate the displayport. the min/max stuff takes care of michael@0: // negative velocities as well as positive. michael@0: RectF margins = new RectF( michael@0: -Math.min(minDx, maxDx), michael@0: -Math.min(minDy, maxDy), michael@0: Math.max(minDx, maxDx), michael@0: Math.max(minDy, maxDy)); michael@0: return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); michael@0: } michael@0: michael@0: @Override michael@0: public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { michael@0: // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs. michael@0: // refer to the comments in calculate() to understand what this is doing. michael@0: float minDx = velocity.x * mMinFramesToDraw; michael@0: float minDy = velocity.y * mMinFramesToDraw; michael@0: float maxDx = velocity.x * mMaxFramesToDraw; michael@0: float maxDy = velocity.y * mMaxFramesToDraw; michael@0: float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy)); michael@0: maxDx = maxDx * pixelsToDraw / mPixelArea; michael@0: maxDy = maxDy * pixelsToDraw / mPixelArea; michael@0: michael@0: // now that we have an idea of how far we will be when the draw completes, take the farthest michael@0: // end of that range and see if it falls outside the displayport bounds. if it does, allow michael@0: // the draw to go through michael@0: RectF predictedViewport = metrics.getViewport(); michael@0: predictedViewport.left += maxDx; michael@0: predictedViewport.top += maxDy; michael@0: predictedViewport.right += maxDx; michael@0: predictedViewport.bottom += maxDy; michael@0: michael@0: predictedViewport = clampToPageBounds(predictedViewport, metrics); michael@0: return !displayPort.contains(predictedViewport); michael@0: } michael@0: michael@0: @Override michael@0: public boolean drawTimeUpdate(long millis, int pixels) { michael@0: // calculate the number of frames it took to draw a viewport-sized area michael@0: float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels; michael@0: int normalizedFrames = (int)FloatMath.ceil(normalizedTime * 60f / 1000f); michael@0: // broaden our range on how long it takes to draw if the draw falls outside michael@0: // the range. this allows it to grow gradually. this heuristic may need to michael@0: // be tweaked into more of a floating window average or something. michael@0: if (normalizedFrames <= mMinFramesToDraw) { michael@0: mMinFramesToDraw--; michael@0: } else if (normalizedFrames > mMaxFramesToDraw) { michael@0: mMaxFramesToDraw++; michael@0: } else { michael@0: return true; michael@0: } michael@0: Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]"); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public void resetPageState() { michael@0: mMinFramesToDraw = 0; michael@0: mMaxFramesToDraw = 2; michael@0: } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD; michael@0: } michael@0: } michael@0: }