1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/DisplayPortCalculator.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,776 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import org.mozilla.gecko.GeckoAppShell; 1.12 +import org.mozilla.gecko.PrefsHelper; 1.13 +import org.mozilla.gecko.util.FloatUtils; 1.14 + 1.15 +import org.json.JSONArray; 1.16 + 1.17 +import android.graphics.PointF; 1.18 +import android.graphics.RectF; 1.19 +import android.util.FloatMath; 1.20 +import android.util.Log; 1.21 + 1.22 +import java.util.HashMap; 1.23 +import java.util.Map; 1.24 + 1.25 +final class DisplayPortCalculator { 1.26 + private static final String LOGTAG = "GeckoDisplayPort"; 1.27 + private static final PointF ZERO_VELOCITY = new PointF(0, 0); 1.28 + 1.29 + // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h 1.30 + private static final int TILE_SIZE = 256; 1.31 + 1.32 + private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy"; 1.33 + private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier"; 1.34 + private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x"; 1.35 + private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y"; 1.36 + private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier"; 1.37 + private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold"; 1.38 + private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer"; 1.39 + private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base"; 1.40 + private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base"; 1.41 + private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr"; 1.42 + private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr"; 1.43 + private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold"; 1.44 + 1.45 + private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null); 1.46 + 1.47 + static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.48 + return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity)); 1.49 + } 1.50 + 1.51 + static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.52 + if (displayPort == null) { 1.53 + return true; 1.54 + } 1.55 + return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort); 1.56 + } 1.57 + 1.58 + static boolean drawTimeUpdate(long millis, int pixels) { 1.59 + return sStrategy.drawTimeUpdate(millis, pixels); 1.60 + } 1.61 + 1.62 + static void resetPageState() { 1.63 + sStrategy.resetPageState(); 1.64 + } 1.65 + 1.66 + static void initPrefs() { 1.67 + final String[] prefs = { PREF_DISPLAYPORT_STRATEGY, 1.68 + PREF_DISPLAYPORT_FM_MULTIPLIER, 1.69 + PREF_DISPLAYPORT_FM_DANGER_X, 1.70 + PREF_DISPLAYPORT_FM_DANGER_Y, 1.71 + PREF_DISPLAYPORT_VB_MULTIPLIER, 1.72 + PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 1.73 + PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 1.74 + PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1.75 + PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1.76 + PREF_DISPLAYPORT_VB_DANGER_X_INCR, 1.77 + PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 1.78 + PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD }; 1.79 + 1.80 + PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() { 1.81 + private Map<String, Integer> mValues = new HashMap<String, Integer>(); 1.82 + 1.83 + @Override public void prefValue(String pref, int value) { 1.84 + mValues.put(pref, value); 1.85 + } 1.86 + 1.87 + @Override public void finish() { 1.88 + setStrategy(mValues); 1.89 + } 1.90 + }); 1.91 + } 1.92 + 1.93 + /** 1.94 + * Set the active strategy to use. 1.95 + * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the 1.96 + * mapping between ints and strategies. 1.97 + */ 1.98 + static boolean setStrategy(Map<String, Integer> prefs) { 1.99 + Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY); 1.100 + if (strategy == null) { 1.101 + return false; 1.102 + } 1.103 + 1.104 + switch (strategy) { 1.105 + case 0: 1.106 + sStrategy = new FixedMarginStrategy(prefs); 1.107 + break; 1.108 + case 1: 1.109 + sStrategy = new VelocityBiasStrategy(prefs); 1.110 + break; 1.111 + case 2: 1.112 + sStrategy = new DynamicResolutionStrategy(prefs); 1.113 + break; 1.114 + case 3: 1.115 + sStrategy = new NoMarginStrategy(prefs); 1.116 + break; 1.117 + case 4: 1.118 + sStrategy = new PredictionBiasStrategy(prefs); 1.119 + break; 1.120 + default: 1.121 + Log.e(LOGTAG, "Invalid strategy index specified"); 1.122 + return false; 1.123 + } 1.124 + Log.i(LOGTAG, "Set strategy " + sStrategy.toString()); 1.125 + return true; 1.126 + } 1.127 + 1.128 + private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) { 1.129 + Integer value = (prefs == null ? null : prefs.get(prefName)); 1.130 + return (float)(value == null || value < 0 ? defaultValue : value) / 1000f; 1.131 + } 1.132 + 1.133 + private static abstract class DisplayPortStrategy { 1.134 + /** Calculates a displayport given a viewport and panning velocity. */ 1.135 + public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity); 1.136 + /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */ 1.137 + public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort); 1.138 + /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */ 1.139 + public boolean drawTimeUpdate(long millis, int pixels) { return false; } 1.140 + /** Reset any page-specific state stored, as the page being displayed has changed. */ 1.141 + public void resetPageState() {} 1.142 + } 1.143 + 1.144 + /** 1.145 + * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the 1.146 + * given metrics object. The area in the returned FloatSize may be less than width*height if the page is 1.147 + * small, but it will never be larger than width*height. 1.148 + * Note that this process may change the relative aspect ratio of the given dimensions. 1.149 + */ 1.150 + private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) { 1.151 + // figure out how much of the desired buffer amount we can actually use on the horizontal axis 1.152 + float usableWidth = Math.min(width, metrics.getPageWidth()); 1.153 + // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and 1.154 + // use it on the vertical axis 1.155 + float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth); 1.156 + float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight()); 1.157 + if (usableHeight < height && usableWidth == width) { 1.158 + // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal 1.159 + float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight); 1.160 + usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth()); 1.161 + } 1.162 + return new FloatSize(usableWidth, usableHeight); 1.163 + } 1.164 + 1.165 + /** 1.166 + * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis 1.167 + * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then 1.168 + * clamped to page bounds and returned. 1.169 + */ 1.170 + private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) { 1.171 + // calculate the danger zone amounts in pixels 1.172 + float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier; 1.173 + float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier; 1.174 + rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY); 1.175 + // clamp to page bounds 1.176 + return clampToPageBounds(rect, metrics); 1.177 + } 1.178 + 1.179 + /** 1.180 + * Expand the given margins such that when they are applied on the viewport, the resulting rect 1.181 + * does not have any partial tiles, except when it is clipped by the page bounds. This assumes 1.182 + * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be 1.183 + * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)). 1.184 + */ 1.185 + private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) { 1.186 + float left = metrics.viewportRectLeft - margins.left; 1.187 + float top = metrics.viewportRectTop - margins.top; 1.188 + float right = metrics.viewportRectRight + margins.right; 1.189 + float bottom = metrics.viewportRectBottom + margins.bottom; 1.190 + left = Math.max(metrics.pageRectLeft, TILE_SIZE * FloatMath.floor(left / TILE_SIZE)); 1.191 + top = Math.max(metrics.pageRectTop, TILE_SIZE * FloatMath.floor(top / TILE_SIZE)); 1.192 + right = Math.min(metrics.pageRectRight, TILE_SIZE * FloatMath.ceil(right / TILE_SIZE)); 1.193 + bottom = Math.min(metrics.pageRectBottom, TILE_SIZE * FloatMath.ceil(bottom / TILE_SIZE)); 1.194 + return new DisplayPortMetrics(left, top, right, bottom, zoom); 1.195 + } 1.196 + 1.197 + /** 1.198 + * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect 1.199 + * does not exceed the page bounds. This code will maintain the total margin amount for a given axis; 1.200 + * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to 1.201 + * metrics.getPageWidth(); and the same for the y axis. 1.202 + */ 1.203 + private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) { 1.204 + // check how much we're overflowing in each direction. note that at most one of leftOverflow 1.205 + // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow 1.206 + // can be greater than zero, because of the assumption described in the method javadoc. 1.207 + float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left); 1.208 + float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight; 1.209 + float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top); 1.210 + float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom; 1.211 + 1.212 + // if the margins overflow the page bounds, shift them to other side on the same axis 1.213 + if (leftOverflow > 0) { 1.214 + margins.left -= leftOverflow; 1.215 + margins.right += leftOverflow; 1.216 + } else if (rightOverflow > 0) { 1.217 + margins.right -= rightOverflow; 1.218 + margins.left += rightOverflow; 1.219 + } 1.220 + if (topOverflow > 0) { 1.221 + margins.top -= topOverflow; 1.222 + margins.bottom += topOverflow; 1.223 + } else if (bottomOverflow > 0) { 1.224 + margins.bottom -= bottomOverflow; 1.225 + margins.top += bottomOverflow; 1.226 + } 1.227 + return margins; 1.228 + } 1.229 + 1.230 + /** 1.231 + * Clamp the given rect to the page bounds and return it. 1.232 + */ 1.233 + private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) { 1.234 + if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop; 1.235 + if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft; 1.236 + if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight; 1.237 + if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom; 1.238 + return rect; 1.239 + } 1.240 + 1.241 + /** 1.242 + * This class implements the variation where we basically don't bother with a a display port. 1.243 + */ 1.244 + private static class NoMarginStrategy extends DisplayPortStrategy { 1.245 + NoMarginStrategy(Map<String, Integer> prefs) { 1.246 + // no prefs in this strategy 1.247 + } 1.248 + 1.249 + @Override 1.250 + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.251 + return new DisplayPortMetrics(metrics.viewportRectLeft, 1.252 + metrics.viewportRectTop, 1.253 + metrics.viewportRectRight, 1.254 + metrics.viewportRectBottom, 1.255 + metrics.zoomFactor); 1.256 + } 1.257 + 1.258 + @Override 1.259 + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.260 + return true; 1.261 + } 1.262 + 1.263 + @Override 1.264 + public String toString() { 1.265 + return "NoMarginStrategy"; 1.266 + } 1.267 + } 1.268 + 1.269 + /** 1.270 + * This class implements the variation where we use a fixed-size margin on the display port. 1.271 + * The margin is always 300 pixels in all directions, except when we are (a) approaching a page 1.272 + * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain 1.273 + * the area of the display port by (a) shifting the buffer to the other side on the same axis, 1.274 + * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on 1.275 + * one axis. 1.276 + */ 1.277 + private static class FixedMarginStrategy extends DisplayPortStrategy { 1.278 + // The length of each axis of the display port will be the corresponding view length 1.279 + // multiplied by this factor. 1.280 + private final float SIZE_MULTIPLIER; 1.281 + 1.282 + // If the visible rect is within the danger zone (measured as a fraction of the view size 1.283 + // from the edge of the displayport) we start redrawing to minimize checkerboarding. 1.284 + private final float DANGER_ZONE_X_MULTIPLIER; 1.285 + private final float DANGER_ZONE_Y_MULTIPLIER; 1.286 + 1.287 + FixedMarginStrategy(Map<String, Integer> prefs) { 1.288 + SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000); 1.289 + DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100); 1.290 + DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200); 1.291 + } 1.292 + 1.293 + @Override 1.294 + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.295 + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; 1.296 + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; 1.297 + 1.298 + // we need to avoid having a display port that is larger than the page, or we will end up 1.299 + // painting things outside the page bounds (bug 729169). we simultaneously need to make 1.300 + // the display port as large as possible so that we redraw less. reshape the display 1.301 + // port dimensions to accomplish this. 1.302 + FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); 1.303 + float horizontalBuffer = usableSize.width - metrics.getWidth(); 1.304 + float verticalBuffer = usableSize.height - metrics.getHeight(); 1.305 + 1.306 + // and now calculate the display port margins based on how much buffer we've decided to use and 1.307 + // the page bounds, ensuring we use all of the available buffer amounts on one side or the other 1.308 + // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is 1.309 + // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer 1.310 + // is split). 1.311 + RectF margins = new RectF(); 1.312 + margins.left = horizontalBuffer / 2.0f; 1.313 + margins.right = horizontalBuffer - margins.left; 1.314 + margins.top = verticalBuffer / 2.0f; 1.315 + margins.bottom = verticalBuffer - margins.top; 1.316 + margins = shiftMarginsForPageBounds(margins, metrics); 1.317 + 1.318 + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); 1.319 + } 1.320 + 1.321 + @Override 1.322 + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.323 + // Increase the size of the viewport based on the danger zone multiplier (and clamp to page 1.324 + // boundaries), and intersect it with the current displayport to determine whether we're 1.325 + // close to checkerboarding. 1.326 + RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics); 1.327 + return !displayPort.contains(adjustedViewport); 1.328 + } 1.329 + 1.330 + @Override 1.331 + public String toString() { 1.332 + return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER; 1.333 + } 1.334 + } 1.335 + 1.336 + /** 1.337 + * This class implements the variation with a small fixed-size margin with velocity bias. 1.338 + * In this variation, the default margins are pretty small relative to the view size, but 1.339 + * they are affected by the panning velocity. Specifically, if we are panning on one axis, 1.340 + * we remove the margins on the other axis because we are likely axis-locked. Also once 1.341 + * we are panning in one direction above a certain threshold velocity, we shift the buffer 1.342 + * so that it is almost entirely in the direction of the pan, with a little bit in the 1.343 + * reverse direction. 1.344 + */ 1.345 + private static class VelocityBiasStrategy extends DisplayPortStrategy { 1.346 + // The length of each axis of the display port will be the corresponding view length 1.347 + // multiplied by this factor. 1.348 + private final float SIZE_MULTIPLIER; 1.349 + // The velocity above which we apply the velocity bias 1.350 + private final float VELOCITY_THRESHOLD; 1.351 + // How much of the buffer to keep in the reverse direction of the velocity 1.352 + private final float REVERSE_BUFFER; 1.353 + // If the visible rect is within the danger zone we start redrawing to minimize 1.354 + // checkerboarding. the danger zone amount is a linear function of the form: 1.355 + // viewportsize * (base + velocity * incr) 1.356 + // where base and incr are configurable values. 1.357 + private final float DANGER_ZONE_BASE_X_MULTIPLIER; 1.358 + private final float DANGER_ZONE_BASE_Y_MULTIPLIER; 1.359 + private final float DANGER_ZONE_INCR_X_MULTIPLIER; 1.360 + private final float DANGER_ZONE_INCR_Y_MULTIPLIER; 1.361 + 1.362 + VelocityBiasStrategy(Map<String, Integer> prefs) { 1.363 + SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000); 1.364 + VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32); 1.365 + REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200); 1.366 + DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000); 1.367 + DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000); 1.368 + DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0); 1.369 + DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0); 1.370 + } 1.371 + 1.372 + /** 1.373 + * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values. 1.374 + * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER 1.375 + * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the 1.376 + * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction 1.377 + * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the 1.378 + * two margins on that axis. 1.379 + */ 1.380 + private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) { 1.381 + RectF margins = new RectF(); 1.382 + 1.383 + if (velocity.x > VELOCITY_THRESHOLD) { 1.384 + margins.left = xAmount * REVERSE_BUFFER; 1.385 + } else if (velocity.x < -VELOCITY_THRESHOLD) { 1.386 + margins.left = xAmount * (1.0f - REVERSE_BUFFER); 1.387 + } else { 1.388 + margins.left = xAmount / 2.0f; 1.389 + } 1.390 + margins.right = xAmount - margins.left; 1.391 + 1.392 + if (velocity.y > VELOCITY_THRESHOLD) { 1.393 + margins.top = yAmount * REVERSE_BUFFER; 1.394 + } else if (velocity.y < -VELOCITY_THRESHOLD) { 1.395 + margins.top = yAmount * (1.0f - REVERSE_BUFFER); 1.396 + } else { 1.397 + margins.top = yAmount / 2.0f; 1.398 + } 1.399 + margins.bottom = yAmount - margins.top; 1.400 + 1.401 + return margins; 1.402 + } 1.403 + 1.404 + @Override 1.405 + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.406 + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; 1.407 + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; 1.408 + 1.409 + // but if we're panning on one axis, set the margins for the other axis to zero since we are likely 1.410 + // axis locked and won't be displaying that extra area. 1.411 + if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) { 1.412 + displayPortHeight = metrics.getHeight(); 1.413 + } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) { 1.414 + displayPortWidth = metrics.getWidth(); 1.415 + } 1.416 + 1.417 + // we need to avoid having a display port that is larger than the page, or we will end up 1.418 + // painting things outside the page bounds (bug 729169). 1.419 + displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth()); 1.420 + displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight()); 1.421 + float horizontalBuffer = displayPortWidth - metrics.getWidth(); 1.422 + float verticalBuffer = displayPortHeight - metrics.getHeight(); 1.423 + 1.424 + // split the buffer amounts into margins based on velocity, and shift it to 1.425 + // take into account the page bounds 1.426 + RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity); 1.427 + margins = shiftMarginsForPageBounds(margins, metrics); 1.428 + 1.429 + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); 1.430 + } 1.431 + 1.432 + @Override 1.433 + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.434 + // calculate the danger zone amounts based on the prefs 1.435 + float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER)); 1.436 + float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER)); 1.437 + // clamp it such that when added to the viewport, they don't exceed page size. 1.438 + // this is a prerequisite to calling shiftMarginsForPageBounds as we do below. 1.439 + dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth()); 1.440 + dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight()); 1.441 + 1.442 + // split the danger zone into margins based on velocity, and ensure it doesn't exceed 1.443 + // page bounds. 1.444 + RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity); 1.445 + dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics); 1.446 + 1.447 + // we're about to checkerboard if the current viewport area + the danger zone margins 1.448 + // fall out of the current displayport anywhere. 1.449 + RectF adjustedViewport = new RectF( 1.450 + metrics.viewportRectLeft - dangerMargins.left, 1.451 + metrics.viewportRectTop - dangerMargins.top, 1.452 + metrics.viewportRectRight + dangerMargins.right, 1.453 + metrics.viewportRectBottom + dangerMargins.bottom); 1.454 + return !displayPort.contains(adjustedViewport); 1.455 + } 1.456 + 1.457 + @Override 1.458 + public String toString() { 1.459 + return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER 1.460 + + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER 1.461 + + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER; 1.462 + } 1.463 + } 1.464 + 1.465 + /** 1.466 + * This class implements the variation where we draw more of the page at low resolution while panning. 1.467 + * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw 1.468 + * resolution to compensate. This results in the same device-pixel area drawn; the compositor then 1.469 + * scales this up to the viewport zoom level. This results in a large area of the page drawn but it 1.470 + * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding, 1.471 + * where we draw less but never even show it on the screen. 1.472 + */ 1.473 + private static class DynamicResolutionStrategy extends DisplayPortStrategy { 1.474 + // The length of each axis of the display port will be the corresponding view length 1.475 + // multiplied by this factor. 1.476 + private static final float SIZE_MULTIPLIER = 1.5f; 1.477 + 1.478 + // The velocity above which we start zooming out the display port to keep up 1.479 + // with the panning. 1.480 + private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f; 1.481 + 1.482 + // How much we increase the display port based on velocity. Assuming no friction and 1.483 + // splitting (see below), this should be be the number of frames (@60fps) between us 1.484 + // calculating the display port and the draw of the *next* display port getting composited 1.485 + // and displayed on the screen. This is because the timeline looks like this: 1.486 + // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan ! 1.487 + // Gecko: \-> draw -> composite / \-> draw -> composite / 1.488 + // The display port calculated on the first "pan" gets composited to the screen at the 1.489 + // first exclamation mark, and remains on the screen until the second exclamation mark. 1.490 + // In order to avoid checkerboarding, that display port must be able to contain all of 1.491 + // the panning until the second exclamation mark, which encompasses two entire draw/composite 1.492 + // cycles. 1.493 + // If we take into account friction, our velocity multiplier should be reduced as the 1.494 + // amount of pan will decrease each time. If we take into account display port splitting, 1.495 + // it should be increased as the splitting means some of the display port will be used to 1.496 + // draw in the opposite direction of the velocity. For now I'm assuming these two cancel 1.497 + // each other out. 1.498 + private static final float VELOCITY_MULTIPLIER = 60.0f; 1.499 + 1.500 + // The following constants adjust how biased the display port is in the direction of panning. 1.501 + // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the 1.502 + // display port "buffer" area, otherwise we use the slow split factor. This is based on the 1.503 + // assumption that if the user is panning fast, they are less likely to reverse directions 1.504 + // and go backwards, so we should spend more of our display port buffer in the direction of 1.505 + // panning. 1.506 + private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f; 1.507 + private static final float FAST_SPLIT_FACTOR = 0.95f; 1.508 + private static final float SLOW_SPLIT_FACTOR = 0.8f; 1.509 + 1.510 + // The following constants are used for viewport prediction; we use them to estimate where 1.511 + // the viewport will be soon and whether or not we should trigger a draw right now. "soon" 1.512 + // in the previous sentence really refers to the amount of time it would take to draw and 1.513 + // composite from the point at which we do the calculation, and that is not really a known 1.514 + // quantity. The velocity multiplier is how much we multiply the velocity by; it has the 1.515 + // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account 1.516 + // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the 1.517 + // viewport size that we use as an extra "danger zone" around the viewport; if this danger 1.518 + // zone falls outside the display port then we are approaching the point at which we will 1.519 + // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is 1.520 + // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the 1.521 + // danger zone, and thus will be constantly drawing. 1.522 + private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f; 1.523 + private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f) 1.524 + 1.525 + DynamicResolutionStrategy(Map<String, Integer> prefs) { 1.526 + // ignore prefs for now 1.527 + } 1.528 + 1.529 + @Override 1.530 + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.531 + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; 1.532 + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; 1.533 + 1.534 + // for resolution calculation purposes, we need to know what the adjusted display port dimensions 1.535 + // would be if we had zero velocity, so calculate that here before we increase the display port 1.536 + // based on velocity. 1.537 + FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); 1.538 + 1.539 + // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their 1.540 + // relative aspect ratio. 1.541 + if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) { 1.542 + float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth, 1.543 + Math.abs(velocity.y) / displayPortHeight); 1.544 + velocityFactor *= VELOCITY_MULTIPLIER; 1.545 + 1.546 + displayPortWidth += (displayPortWidth * velocityFactor); 1.547 + displayPortHeight += (displayPortHeight * velocityFactor); 1.548 + } 1.549 + 1.550 + // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels) 1.551 + // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied 1.552 + // by metrics.zoomFactor 1.553 + 1.554 + // we need to avoid having a display port that is larger than the page, or we will end up 1.555 + // painting things outside the page bounds (bug 729169). we simultaneously need to make 1.556 + // the display port as large as possible so that we redraw less. reshape the display 1.557 + // port dimensions to accomplish this. this may change the aspect ratio of the display port, 1.558 + // but we are assuming that this is desirable because the advantages from pre-drawing will 1.559 + // outweigh the disadvantages from any buffer reallocations that might occur. 1.560 + FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); 1.561 + float horizontalBuffer = usableSize.width - metrics.getWidth(); 1.562 + float verticalBuffer = usableSize.height - metrics.getHeight(); 1.563 + 1.564 + // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have. 1.565 + // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case 1.566 + // the user scrolls there. we now need to split the buffer area on each axis so that we know 1.567 + // what the exact margins on each side will be. first we split the buffer amount based on the direction 1.568 + // we're moving, so that we have a larger buffer in the direction of travel. 1.569 + RectF margins = new RectF(); 1.570 + margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x); 1.571 + margins.right = horizontalBuffer - margins.left; 1.572 + margins.top = splitBufferByVelocity(verticalBuffer, velocity.y); 1.573 + margins.bottom = verticalBuffer - margins.top; 1.574 + 1.575 + // then, we account for running into the page bounds - so that if we hit the top of the page, we need 1.576 + // to drop the top margin and move that amount to the bottom margin. 1.577 + margins = shiftMarginsForPageBounds(margins, metrics); 1.578 + 1.579 + // finally, we calculate the resolution we want to render the display port area at. We do this 1.580 + // so that as we expand the display port area (because of velocity), we reduce the resolution of 1.581 + // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate 1.582 + // the reduction in resolution by comparing the display port size with and without the velocity 1.583 + // changes applied. 1.584 + // this effectively means that as we pan faster and faster, the display port grows, but we paint 1.585 + // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing 1.586 + // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone. 1.587 + // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we 1.588 + // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor. 1.589 + float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height); 1.590 + float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor); 1.591 + 1.592 + DisplayPortMetrics dpMetrics = new DisplayPortMetrics( 1.593 + metrics.viewportRectLeft - margins.left, 1.594 + metrics.viewportRectTop - margins.top, 1.595 + metrics.viewportRectRight + margins.right, 1.596 + metrics.viewportRectBottom + margins.bottom, 1.597 + displayResolution); 1.598 + return dpMetrics; 1.599 + } 1.600 + 1.601 + /** 1.602 + * Split the given buffer amount into two based on the velocity. 1.603 + * Given an amount of total usable buffer on an axis, this will 1.604 + * return the amount that should be used on the left/top side of 1.605 + * the axis (the side which a negative velocity vector corresponds 1.606 + * to). 1.607 + */ 1.608 + private float splitBufferByVelocity(float amount, float velocity) { 1.609 + // if no velocity, so split evenly 1.610 + if (FloatUtils.fuzzyEquals(velocity, 0)) { 1.611 + return amount / 2.0f; 1.612 + } 1.613 + // if we're moving quickly, assign more of the amount in that direction 1.614 + // since is is less likely that we will reverse direction immediately 1.615 + if (velocity < -VELOCITY_FAST_THRESHOLD) { 1.616 + return amount * FAST_SPLIT_FACTOR; 1.617 + } 1.618 + if (velocity > VELOCITY_FAST_THRESHOLD) { 1.619 + return amount * (1.0f - FAST_SPLIT_FACTOR); 1.620 + } 1.621 + // if we're moving slowly, then assign less of the amount in that direction 1.622 + if (velocity < 0) { 1.623 + return amount * SLOW_SPLIT_FACTOR; 1.624 + } else { 1.625 + return amount * (1.0f - SLOW_SPLIT_FACTOR); 1.626 + } 1.627 + } 1.628 + 1.629 + @Override 1.630 + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.631 + // Expand the viewport based on our velocity (and clamp it to page boundaries). 1.632 + // Then intersect it with the last-requested displayport to determine whether we're 1.633 + // close to checkerboarding. 1.634 + 1.635 + RectF predictedViewport = metrics.getViewport(); 1.636 + 1.637 + // first we expand the viewport in the direction we're moving based on some 1.638 + // multiple of the current velocity. 1.639 + if (velocity.length() > 0) { 1.640 + if (velocity.x < 0) { 1.641 + predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; 1.642 + } else if (velocity.x > 0) { 1.643 + predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; 1.644 + } 1.645 + 1.646 + if (velocity.y < 0) { 1.647 + predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; 1.648 + } else if (velocity.y > 0) { 1.649 + predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; 1.650 + } 1.651 + } 1.652 + 1.653 + // then we expand the viewport evenly in all directions just to have an extra 1.654 + // safety zone. this also clamps it to page bounds. 1.655 + predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics); 1.656 + return !displayPort.contains(predictedViewport); 1.657 + } 1.658 + 1.659 + @Override 1.660 + public String toString() { 1.661 + return "DynamicResolutionStrategy"; 1.662 + } 1.663 + } 1.664 + 1.665 + /** 1.666 + * This class implements the variation where we use the draw time to predict where we will be when 1.667 + * a draw completes, and draw that instead of where we are now. In this variation, when our panning 1.668 + * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can 1.669 + * pan in any direction without encountering checkerboarding. 1.670 + * Once the user is panning, we modify the displayport to encompass an area range of where we think 1.671 + * the user will be when the draw completes. This heuristic relies on both the estimated draw time 1.672 + * the panning velocity; unexpected changes in either of these values will cause the heuristic to 1.673 + * fail and show checkerboard. 1.674 + */ 1.675 + private static class PredictionBiasStrategy extends DisplayPortStrategy { 1.676 + private static float VELOCITY_THRESHOLD; 1.677 + 1.678 + private int mPixelArea; // area of the viewport, used in draw time calculations 1.679 + private int mMinFramesToDraw; // minimum number of frames we take to draw 1.680 + private int mMaxFramesToDraw; // maximum number of frames we take to draw 1.681 + 1.682 + PredictionBiasStrategy(Map<String, Integer> prefs) { 1.683 + VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16); 1.684 + resetPageState(); 1.685 + } 1.686 + 1.687 + @Override 1.688 + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { 1.689 + float width = metrics.getWidth(); 1.690 + float height = metrics.getHeight(); 1.691 + mPixelArea = (int)(width * height); 1.692 + 1.693 + if (velocity.length() < VELOCITY_THRESHOLD) { 1.694 + // if we're going slow, expand the displayport to 9x viewport size 1.695 + RectF margins = new RectF(width, height, width, height); 1.696 + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); 1.697 + } 1.698 + 1.699 + // figure out how far we expect to be 1.700 + float minDx = velocity.x * mMinFramesToDraw; 1.701 + float minDy = velocity.y * mMinFramesToDraw; 1.702 + float maxDx = velocity.x * mMaxFramesToDraw; 1.703 + float maxDy = velocity.y * mMaxFramesToDraw; 1.704 + 1.705 + // figure out how many pixels we will be drawing when we draw the above-calculated range. 1.706 + // this will be larger than the viewport area. 1.707 + float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy)); 1.708 + // adjust how far we will get because of the time spent drawing all these extra pixels. this 1.709 + // will again increase the number of pixels drawn so really we could keep iterating this over 1.710 + // and over, but once seems enough for now. 1.711 + maxDx = maxDx * pixelsToDraw / mPixelArea; 1.712 + maxDy = maxDy * pixelsToDraw / mPixelArea; 1.713 + 1.714 + // and finally generate the displayport. the min/max stuff takes care of 1.715 + // negative velocities as well as positive. 1.716 + RectF margins = new RectF( 1.717 + -Math.min(minDx, maxDx), 1.718 + -Math.min(minDy, maxDy), 1.719 + Math.max(minDx, maxDx), 1.720 + Math.max(minDy, maxDy)); 1.721 + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); 1.722 + } 1.723 + 1.724 + @Override 1.725 + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { 1.726 + // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs. 1.727 + // refer to the comments in calculate() to understand what this is doing. 1.728 + float minDx = velocity.x * mMinFramesToDraw; 1.729 + float minDy = velocity.y * mMinFramesToDraw; 1.730 + float maxDx = velocity.x * mMaxFramesToDraw; 1.731 + float maxDy = velocity.y * mMaxFramesToDraw; 1.732 + float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy)); 1.733 + maxDx = maxDx * pixelsToDraw / mPixelArea; 1.734 + maxDy = maxDy * pixelsToDraw / mPixelArea; 1.735 + 1.736 + // now that we have an idea of how far we will be when the draw completes, take the farthest 1.737 + // end of that range and see if it falls outside the displayport bounds. if it does, allow 1.738 + // the draw to go through 1.739 + RectF predictedViewport = metrics.getViewport(); 1.740 + predictedViewport.left += maxDx; 1.741 + predictedViewport.top += maxDy; 1.742 + predictedViewport.right += maxDx; 1.743 + predictedViewport.bottom += maxDy; 1.744 + 1.745 + predictedViewport = clampToPageBounds(predictedViewport, metrics); 1.746 + return !displayPort.contains(predictedViewport); 1.747 + } 1.748 + 1.749 + @Override 1.750 + public boolean drawTimeUpdate(long millis, int pixels) { 1.751 + // calculate the number of frames it took to draw a viewport-sized area 1.752 + float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels; 1.753 + int normalizedFrames = (int)FloatMath.ceil(normalizedTime * 60f / 1000f); 1.754 + // broaden our range on how long it takes to draw if the draw falls outside 1.755 + // the range. this allows it to grow gradually. this heuristic may need to 1.756 + // be tweaked into more of a floating window average or something. 1.757 + if (normalizedFrames <= mMinFramesToDraw) { 1.758 + mMinFramesToDraw--; 1.759 + } else if (normalizedFrames > mMaxFramesToDraw) { 1.760 + mMaxFramesToDraw++; 1.761 + } else { 1.762 + return true; 1.763 + } 1.764 + Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]"); 1.765 + return true; 1.766 + } 1.767 + 1.768 + @Override 1.769 + public void resetPageState() { 1.770 + mMinFramesToDraw = 0; 1.771 + mMaxFramesToDraw = 2; 1.772 + } 1.773 + 1.774 + @Override 1.775 + public String toString() { 1.776 + return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD; 1.777 + } 1.778 + } 1.779 +}