mobile/android/base/gfx/DisplayPortCalculator.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     4  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.gfx;
     8 import org.mozilla.gecko.GeckoAppShell;
     9 import org.mozilla.gecko.PrefsHelper;
    10 import org.mozilla.gecko.util.FloatUtils;
    12 import org.json.JSONArray;
    14 import android.graphics.PointF;
    15 import android.graphics.RectF;
    16 import android.util.FloatMath;
    17 import android.util.Log;
    19 import java.util.HashMap;
    20 import java.util.Map;
    22 final class DisplayPortCalculator {
    23     private static final String LOGTAG = "GeckoDisplayPort";
    24     private static final PointF ZERO_VELOCITY = new PointF(0, 0);
    26     // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h
    27     private static final int TILE_SIZE = 256;
    29     private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
    30     private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
    31     private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
    32     private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
    33     private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
    34     private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
    35     private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
    36     private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
    37     private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base";
    38     private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr";
    39     private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr";
    40     private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold";
    42     private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
    44     static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
    45         return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
    46     }
    48     static boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
    49         if (displayPort == null) {
    50             return true;
    51         }
    52         return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort);
    53     }
    55     static boolean drawTimeUpdate(long millis, int pixels) {
    56         return sStrategy.drawTimeUpdate(millis, pixels);
    57     }
    59     static void resetPageState() {
    60         sStrategy.resetPageState();
    61     }
    63     static void initPrefs() {
    64         final String[] prefs = { PREF_DISPLAYPORT_STRATEGY,
    65                                  PREF_DISPLAYPORT_FM_MULTIPLIER,
    66                                  PREF_DISPLAYPORT_FM_DANGER_X,
    67                                  PREF_DISPLAYPORT_FM_DANGER_Y,
    68                                  PREF_DISPLAYPORT_VB_MULTIPLIER,
    69                                  PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD,
    70                                  PREF_DISPLAYPORT_VB_REVERSE_BUFFER,
    71                                  PREF_DISPLAYPORT_VB_DANGER_X_BASE,
    72                                  PREF_DISPLAYPORT_VB_DANGER_Y_BASE,
    73                                  PREF_DISPLAYPORT_VB_DANGER_X_INCR,
    74                                  PREF_DISPLAYPORT_VB_DANGER_Y_INCR,
    75                                  PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD };
    77         PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
    78             private Map<String, Integer> mValues = new HashMap<String, Integer>();
    80             @Override public void prefValue(String pref, int value) {
    81                 mValues.put(pref, value);
    82             }
    84             @Override public void finish() {
    85                 setStrategy(mValues);
    86             }
    87         });
    88     }
    90     /**
    91      * Set the active strategy to use.
    92      * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the
    93      * mapping between ints and strategies.
    94      */
    95     static boolean setStrategy(Map<String, Integer> prefs) {
    96         Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY);
    97         if (strategy == null) {
    98             return false;
    99         }
   101         switch (strategy) {
   102             case 0:
   103                 sStrategy = new FixedMarginStrategy(prefs);
   104                 break;
   105             case 1:
   106                 sStrategy = new VelocityBiasStrategy(prefs);
   107                 break;
   108             case 2:
   109                 sStrategy = new DynamicResolutionStrategy(prefs);
   110                 break;
   111             case 3:
   112                 sStrategy = new NoMarginStrategy(prefs);
   113                 break;
   114             case 4:
   115                 sStrategy = new PredictionBiasStrategy(prefs);
   116                 break;
   117             default:
   118                 Log.e(LOGTAG, "Invalid strategy index specified");
   119                 return false;
   120         }
   121         Log.i(LOGTAG, "Set strategy " + sStrategy.toString());
   122         return true;
   123     }
   125     private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
   126         Integer value = (prefs == null ? null : prefs.get(prefName));
   127         return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
   128     }
   130     private static abstract class DisplayPortStrategy {
   131         /** Calculates a displayport given a viewport and panning velocity. */
   132         public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity);
   133         /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */
   134         public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort);
   135         /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */
   136         public boolean drawTimeUpdate(long millis, int pixels) { return false; }
   137         /** Reset any page-specific state stored, as the page being displayed has changed. */
   138         public void resetPageState() {}
   139     }
   141     /**
   142      * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the
   143      * given metrics object. The area in the returned FloatSize may be less than width*height if the page is
   144      * small, but it will never be larger than width*height.
   145      * Note that this process may change the relative aspect ratio of the given dimensions.
   146      */
   147     private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) {
   148         // figure out how much of the desired buffer amount we can actually use on the horizontal axis
   149         float usableWidth = Math.min(width, metrics.getPageWidth());
   150         // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and
   151         // use it on the vertical axis
   152         float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth);
   153         float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight());
   154         if (usableHeight < height && usableWidth == width) {
   155             // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal
   156             float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight);
   157             usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth());
   158         }
   159         return new FloatSize(usableWidth, usableHeight);
   160     }
   162     /**
   163      * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis
   164      * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then
   165      * clamped to page bounds and returned.
   166      */
   167     private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) {
   168         // calculate the danger zone amounts in pixels
   169         float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
   170         float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
   171         rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
   172         // clamp to page bounds
   173         return clampToPageBounds(rect, metrics);
   174     }
   176     /**
   177      * Expand the given margins such that when they are applied on the viewport, the resulting rect
   178      * does not have any partial tiles, except when it is clipped by the page bounds. This assumes
   179      * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be
   180      * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)).
   181      */
   182     private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
   183         float left = metrics.viewportRectLeft - margins.left;
   184         float top = metrics.viewportRectTop - margins.top;
   185         float right = metrics.viewportRectRight + margins.right;
   186         float bottom = metrics.viewportRectBottom + margins.bottom;
   187         left = Math.max(metrics.pageRectLeft, TILE_SIZE * FloatMath.floor(left / TILE_SIZE));
   188         top = Math.max(metrics.pageRectTop, TILE_SIZE * FloatMath.floor(top / TILE_SIZE));
   189         right = Math.min(metrics.pageRectRight, TILE_SIZE * FloatMath.ceil(right / TILE_SIZE));
   190         bottom = Math.min(metrics.pageRectBottom, TILE_SIZE * FloatMath.ceil(bottom / TILE_SIZE));
   191         return new DisplayPortMetrics(left, top, right, bottom, zoom);
   192     }
   194     /**
   195      * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
   196      * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
   197      * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
   198      * metrics.getPageWidth(); and the same for the y axis.
   199      */
   200     private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) {
   201         // check how much we're overflowing in each direction. note that at most one of leftOverflow
   202         // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow
   203         // can be greater than zero, because of the assumption described in the method javadoc.
   204         float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left);
   205         float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight;
   206         float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top);
   207         float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom;
   209         // if the margins overflow the page bounds, shift them to other side on the same axis
   210         if (leftOverflow > 0) {
   211             margins.left -= leftOverflow;
   212             margins.right += leftOverflow;
   213         } else if (rightOverflow > 0) {
   214             margins.right -= rightOverflow;
   215             margins.left += rightOverflow;
   216         }
   217         if (topOverflow > 0) {
   218             margins.top -= topOverflow;
   219             margins.bottom += topOverflow;
   220         } else if (bottomOverflow > 0) {
   221             margins.bottom -= bottomOverflow;
   222             margins.top += bottomOverflow;
   223         }
   224         return margins;
   225     }
   227     /**
   228      * Clamp the given rect to the page bounds and return it.
   229      */
   230     private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) {
   231         if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop;
   232         if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft;
   233         if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight;
   234         if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom;
   235         return rect;
   236     }
   238     /**
   239      * This class implements the variation where we basically don't bother with a a display port.
   240      */
   241     private static class NoMarginStrategy extends DisplayPortStrategy {
   242         NoMarginStrategy(Map<String, Integer> prefs) {
   243             // no prefs in this strategy
   244         }
   246         @Override
   247         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
   248             return new DisplayPortMetrics(metrics.viewportRectLeft,
   249                     metrics.viewportRectTop,
   250                     metrics.viewportRectRight,
   251                     metrics.viewportRectBottom,
   252                     metrics.zoomFactor);
   253         }
   255         @Override
   256         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
   257             return true;
   258         }
   260         @Override
   261         public String toString() {
   262             return "NoMarginStrategy";
   263         }
   264     }
   266     /**
   267      * This class implements the variation where we use a fixed-size margin on the display port.
   268      * The margin is always 300 pixels in all directions, except when we are (a) approaching a page
   269      * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain
   270      * the area of the display port by (a) shifting the buffer to the other side on the same axis,
   271      * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on
   272      * one axis.
   273      */
   274     private static class FixedMarginStrategy extends DisplayPortStrategy {
   275         // The length of each axis of the display port will be the corresponding view length
   276         // multiplied by this factor.
   277         private final float SIZE_MULTIPLIER;
   279         // If the visible rect is within the danger zone (measured as a fraction of the view size
   280         // from the edge of the displayport) we start redrawing to minimize checkerboarding.
   281         private final float DANGER_ZONE_X_MULTIPLIER;
   282         private final float DANGER_ZONE_Y_MULTIPLIER;
   284         FixedMarginStrategy(Map<String, Integer> prefs) {
   285             SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000);
   286             DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100);
   287             DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200);
   288         }
   290         @Override
   291         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
   292             float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
   293             float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
   295             // we need to avoid having a display port that is larger than the page, or we will end up
   296             // painting things outside the page bounds (bug 729169). we simultaneously need to make
   297             // the display port as large as possible so that we redraw less. reshape the display
   298             // port dimensions to accomplish this.
   299             FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
   300             float horizontalBuffer = usableSize.width - metrics.getWidth();
   301             float verticalBuffer = usableSize.height - metrics.getHeight();
   303             // and now calculate the display port margins based on how much buffer we've decided to use and
   304             // the page bounds, ensuring we use all of the available buffer amounts on one side or the other
   305             // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is
   306             // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer
   307             // is split).
   308             RectF margins = new RectF();
   309             margins.left = horizontalBuffer / 2.0f;
   310             margins.right = horizontalBuffer - margins.left;
   311             margins.top = verticalBuffer / 2.0f;
   312             margins.bottom = verticalBuffer - margins.top;
   313             margins = shiftMarginsForPageBounds(margins, metrics);
   315             return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
   316         }
   318         @Override
   319         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
   320             // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
   321             // boundaries), and intersect it with the current displayport to determine whether we're
   322             // close to checkerboarding.
   323             RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
   324             return !displayPort.contains(adjustedViewport);
   325         }
   327         @Override
   328         public String toString() {
   329             return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
   330         }
   331     }
   333     /**
   334      * This class implements the variation with a small fixed-size margin with velocity bias.
   335      * In this variation, the default margins are pretty small relative to the view size, but
   336      * they are affected by the panning velocity. Specifically, if we are panning on one axis,
   337      * we remove the margins on the other axis because we are likely axis-locked. Also once
   338      * we are panning in one direction above a certain threshold velocity, we shift the buffer
   339      * so that it is almost entirely in the direction of the pan, with a little bit in the
   340      * reverse direction.
   341      */
   342     private static class VelocityBiasStrategy extends DisplayPortStrategy {
   343         // The length of each axis of the display port will be the corresponding view length
   344         // multiplied by this factor.
   345         private final float SIZE_MULTIPLIER;
   346         // The velocity above which we apply the velocity bias
   347         private final float VELOCITY_THRESHOLD;
   348         // How much of the buffer to keep in the reverse direction of the velocity
   349         private final float REVERSE_BUFFER;
   350         // If the visible rect is within the danger zone we start redrawing to minimize
   351         // checkerboarding. the danger zone amount is a linear function of the form:
   352         //    viewportsize * (base + velocity * incr)
   353         // where base and incr are configurable values.
   354         private final float DANGER_ZONE_BASE_X_MULTIPLIER;
   355         private final float DANGER_ZONE_BASE_Y_MULTIPLIER;
   356         private final float DANGER_ZONE_INCR_X_MULTIPLIER;
   357         private final float DANGER_ZONE_INCR_Y_MULTIPLIER;
   359         VelocityBiasStrategy(Map<String, Integer> prefs) {
   360             SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000);
   361             VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32);
   362             REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200);
   363             DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000);
   364             DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000);
   365             DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0);
   366             DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0);
   367         }
   369         /**
   370          * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values.
   371          * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER
   372          * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the
   373          * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction
   374          * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the
   375          * two margins on that axis.
   376          */
   377         private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) {
   378             RectF margins = new RectF();
   380             if (velocity.x > VELOCITY_THRESHOLD) {
   381                 margins.left = xAmount * REVERSE_BUFFER;
   382             } else if (velocity.x < -VELOCITY_THRESHOLD) {
   383                 margins.left = xAmount * (1.0f - REVERSE_BUFFER);
   384             } else {
   385                 margins.left = xAmount / 2.0f;
   386             }
   387             margins.right = xAmount - margins.left;
   389             if (velocity.y > VELOCITY_THRESHOLD) {
   390                 margins.top = yAmount * REVERSE_BUFFER;
   391             } else if (velocity.y < -VELOCITY_THRESHOLD) {
   392                 margins.top = yAmount * (1.0f - REVERSE_BUFFER);
   393             } else {
   394                 margins.top = yAmount / 2.0f;
   395             }
   396             margins.bottom = yAmount - margins.top;
   398             return margins;
   399         }
   401         @Override
   402         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
   403             float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
   404             float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
   406             // but if we're panning on one axis, set the margins for the other axis to zero since we are likely
   407             // axis locked and won't be displaying that extra area.
   408             if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) {
   409                 displayPortHeight = metrics.getHeight();
   410             } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) {
   411                 displayPortWidth = metrics.getWidth();
   412             }
   414             // we need to avoid having a display port that is larger than the page, or we will end up
   415             // painting things outside the page bounds (bug 729169).
   416             displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth());
   417             displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight());
   418             float horizontalBuffer = displayPortWidth - metrics.getWidth();
   419             float verticalBuffer = displayPortHeight - metrics.getHeight();
   421             // split the buffer amounts into margins based on velocity, and shift it to
   422             // take into account the page bounds
   423             RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
   424             margins = shiftMarginsForPageBounds(margins, metrics);
   426             return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
   427         }
   429         @Override
   430         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
   431             // calculate the danger zone amounts based on the prefs
   432             float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
   433             float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
   434             // clamp it such that when added to the viewport, they don't exceed page size.
   435             // this is a prerequisite to calling shiftMarginsForPageBounds as we do below.
   436             dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth());
   437             dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight());
   439             // split the danger zone into margins based on velocity, and ensure it doesn't exceed
   440             // page bounds.
   441             RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity);
   442             dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics);
   444             // we're about to checkerboard if the current viewport area + the danger zone margins
   445             // fall out of the current displayport anywhere.
   446             RectF adjustedViewport = new RectF(
   447                     metrics.viewportRectLeft - dangerMargins.left,
   448                     metrics.viewportRectTop - dangerMargins.top,
   449                     metrics.viewportRectRight + dangerMargins.right,
   450                     metrics.viewportRectBottom + dangerMargins.bottom);
   451             return !displayPort.contains(adjustedViewport);
   452         }
   454         @Override
   455         public String toString() {
   456             return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER
   457                 + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER
   458                 + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER;
   459         }
   460     }
   462     /**
   463      * This class implements the variation where we draw more of the page at low resolution while panning.
   464      * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw
   465      * resolution to compensate. This results in the same device-pixel area drawn; the compositor then
   466      * scales this up to the viewport zoom level. This results in a large area of the page drawn but it
   467      * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding,
   468      * where we draw less but never even show it on the screen.
   469      */
   470     private static class DynamicResolutionStrategy extends DisplayPortStrategy {
   471         // The length of each axis of the display port will be the corresponding view length
   472         // multiplied by this factor.
   473         private static final float SIZE_MULTIPLIER = 1.5f;
   475         // The velocity above which we start zooming out the display port to keep up
   476         // with the panning.
   477         private static final float VELOCITY_EXPANSION_THRESHOLD = GeckoAppShell.getDpi() / 16f;
   479         // How much we increase the display port based on velocity. Assuming no friction and
   480         // splitting (see below), this should be be the number of frames (@60fps) between us
   481         // calculating the display port and the draw of the *next* display port getting composited
   482         // and displayed on the screen. This is because the timeline looks like this:
   483         //      Java: pan pan pan pan pan pan ! pan pan pan pan pan pan !
   484         //     Gecko:   \-> draw -> composite /   \-> draw -> composite /
   485         // The display port calculated on the first "pan" gets composited to the screen at the
   486         // first exclamation mark, and remains on the screen until the second exclamation mark.
   487         // In order to avoid checkerboarding, that display port must be able to contain all of
   488         // the panning until the second exclamation mark, which encompasses two entire draw/composite
   489         // cycles.
   490         // If we take into account friction, our velocity multiplier should be reduced as the
   491         // amount of pan will decrease each time. If we take into account display port splitting,
   492         // it should be increased as the splitting means some of the display port will be used to
   493         // draw in the opposite direction of the velocity. For now I'm assuming these two cancel
   494         // each other out.
   495         private static final float VELOCITY_MULTIPLIER = 60.0f;
   497         // The following constants adjust how biased the display port is in the direction of panning.
   498         // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the
   499         // display port "buffer" area, otherwise we use the slow split factor. This is based on the
   500         // assumption that if the user is panning fast, they are less likely to reverse directions
   501         // and go backwards, so we should spend more of our display port buffer in the direction of
   502         // panning.
   503         private static final float VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f;
   504         private static final float FAST_SPLIT_FACTOR = 0.95f;
   505         private static final float SLOW_SPLIT_FACTOR = 0.8f;
   507         // The following constants are used for viewport prediction; we use them to estimate where
   508         // the viewport will be soon and whether or not we should trigger a draw right now. "soon"
   509         // in the previous sentence really refers to the amount of time it would take to draw and
   510         // composite from the point at which we do the calculation, and that is not really a known
   511         // quantity. The velocity multiplier is how much we multiply the velocity by; it has the
   512         // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account
   513         // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the
   514         // viewport size that we use as an extra "danger zone" around the viewport; if this danger
   515         // zone falls outside the display port then we are approaching the point at which we will
   516         // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is
   517         // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the
   518         // danger zone, and thus will be constantly drawing.
   519         private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f;
   520         private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f)
   522         DynamicResolutionStrategy(Map<String, Integer> prefs) {
   523             // ignore prefs for now
   524         }
   526         @Override
   527         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
   528             float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
   529             float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
   531             // for resolution calculation purposes, we need to know what the adjusted display port dimensions
   532             // would be if we had zero velocity, so calculate that here before we increase the display port
   533             // based on velocity.
   534             FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
   536             // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their
   537             // relative aspect ratio.
   538             if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) {
   539                 float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth,
   540                                                 Math.abs(velocity.y) / displayPortHeight);
   541                 velocityFactor *= VELOCITY_MULTIPLIER;
   543                 displayPortWidth += (displayPortWidth * velocityFactor);
   544                 displayPortHeight += (displayPortHeight * velocityFactor);
   545             }
   547             // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels)
   548             // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied
   549             // by metrics.zoomFactor
   551             // we need to avoid having a display port that is larger than the page, or we will end up
   552             // painting things outside the page bounds (bug 729169). we simultaneously need to make
   553             // the display port as large as possible so that we redraw less. reshape the display
   554             // port dimensions to accomplish this. this may change the aspect ratio of the display port,
   555             // but we are assuming that this is desirable because the advantages from pre-drawing will
   556             // outweigh the disadvantages from any buffer reallocations that might occur.
   557             FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics);
   558             float horizontalBuffer = usableSize.width - metrics.getWidth();
   559             float verticalBuffer = usableSize.height - metrics.getHeight();
   561             // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have.
   562             // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case
   563             // the user scrolls there. we now need to split the buffer area on each axis so that we know
   564             // what the exact margins on each side will be. first we split the buffer amount based on the direction
   565             // we're moving, so that we have a larger buffer in the direction of travel.
   566             RectF margins = new RectF();
   567             margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x);
   568             margins.right = horizontalBuffer - margins.left;
   569             margins.top = splitBufferByVelocity(verticalBuffer, velocity.y);
   570             margins.bottom = verticalBuffer - margins.top;
   572             // then, we account for running into the page bounds - so that if we hit the top of the page, we need
   573             // to drop the top margin and move that amount to the bottom margin.
   574             margins = shiftMarginsForPageBounds(margins, metrics);
   576             // finally, we calculate the resolution we want to render the display port area at. We do this
   577             // so that as we expand the display port area (because of velocity), we reduce the resolution of
   578             // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate
   579             // the reduction in resolution by comparing the display port size with and without the velocity
   580             // changes applied.
   581             // this effectively means that as we pan faster and faster, the display port grows, but we paint
   582             // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing
   583             // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone.
   584             // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we
   585             // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor.
   586             float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height);
   587             float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor);
   589             DisplayPortMetrics dpMetrics = new DisplayPortMetrics(
   590                 metrics.viewportRectLeft - margins.left,
   591                 metrics.viewportRectTop - margins.top,
   592                 metrics.viewportRectRight + margins.right,
   593                 metrics.viewportRectBottom + margins.bottom,
   594                 displayResolution);
   595             return dpMetrics;
   596         }
   598         /**
   599          * Split the given buffer amount into two based on the velocity.
   600          * Given an amount of total usable buffer on an axis, this will
   601          * return the amount that should be used on the left/top side of
   602          * the axis (the side which a negative velocity vector corresponds
   603          * to).
   604          */
   605         private float splitBufferByVelocity(float amount, float velocity) {
   606             // if no velocity, so split evenly
   607             if (FloatUtils.fuzzyEquals(velocity, 0)) {
   608                 return amount / 2.0f;
   609             }
   610             // if we're moving quickly, assign more of the amount in that direction
   611             // since is is less likely that we will reverse direction immediately
   612             if (velocity < -VELOCITY_FAST_THRESHOLD) {
   613                 return amount * FAST_SPLIT_FACTOR;
   614             }
   615             if (velocity > VELOCITY_FAST_THRESHOLD) {
   616                 return amount * (1.0f - FAST_SPLIT_FACTOR);
   617             }
   618             // if we're moving slowly, then assign less of the amount in that direction
   619             if (velocity < 0) {
   620                 return amount * SLOW_SPLIT_FACTOR;
   621             } else {
   622                 return amount * (1.0f - SLOW_SPLIT_FACTOR);
   623             }
   624         }
   626         @Override
   627         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
   628             // Expand the viewport based on our velocity (and clamp it to page boundaries).
   629             // Then intersect it with the last-requested displayport to determine whether we're
   630             // close to checkerboarding.
   632             RectF predictedViewport = metrics.getViewport();
   634             // first we expand the viewport in the direction we're moving based on some
   635             // multiple of the current velocity.
   636             if (velocity.length() > 0) {
   637                 if (velocity.x < 0) {
   638                     predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
   639                 } else if (velocity.x > 0) {
   640                     predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER;
   641                 }
   643                 if (velocity.y < 0) {
   644                     predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
   645                 } else if (velocity.y > 0) {
   646                     predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER;
   647                 }
   648             }
   650             // then we expand the viewport evenly in all directions just to have an extra
   651             // safety zone. this also clamps it to page bounds.
   652             predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics);
   653             return !displayPort.contains(predictedViewport);
   654         }
   656         @Override
   657         public String toString() {
   658             return "DynamicResolutionStrategy";
   659         }
   660     }
   662     /**
   663      * This class implements the variation where we use the draw time to predict where we will be when
   664      * a draw completes, and draw that instead of where we are now. In this variation, when our panning
   665      * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can
   666      * pan in any direction without encountering checkerboarding.
   667      * Once the user is panning, we modify the displayport to encompass an area range of where we think
   668      * the user will be when the draw completes. This heuristic relies on both the estimated draw time
   669      * the panning velocity; unexpected changes in either of these values will cause the heuristic to
   670      * fail and show checkerboard.
   671      */
   672     private static class PredictionBiasStrategy extends DisplayPortStrategy {
   673         private static float VELOCITY_THRESHOLD;
   675         private int mPixelArea;         // area of the viewport, used in draw time calculations
   676         private int mMinFramesToDraw;   // minimum number of frames we take to draw
   677         private int mMaxFramesToDraw;   // maximum number of frames we take to draw
   679         PredictionBiasStrategy(Map<String, Integer> prefs) {
   680             VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
   681             resetPageState();
   682         }
   684         @Override
   685         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
   686             float width = metrics.getWidth();
   687             float height = metrics.getHeight();
   688             mPixelArea = (int)(width * height);
   690             if (velocity.length() < VELOCITY_THRESHOLD) {
   691                 // if we're going slow, expand the displayport to 9x viewport size
   692                 RectF margins = new RectF(width, height, width, height);
   693                 return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
   694             }
   696             // figure out how far we expect to be
   697             float minDx = velocity.x * mMinFramesToDraw;
   698             float minDy = velocity.y * mMinFramesToDraw;
   699             float maxDx = velocity.x * mMaxFramesToDraw;
   700             float maxDy = velocity.y * mMaxFramesToDraw;
   702             // figure out how many pixels we will be drawing when we draw the above-calculated range.
   703             // this will be larger than the viewport area.
   704             float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy));
   705             // adjust how far we will get because of the time spent drawing all these extra pixels. this
   706             // will again increase the number of pixels drawn so really we could keep iterating this over
   707             // and over, but once seems enough for now.
   708             maxDx = maxDx * pixelsToDraw / mPixelArea;
   709             maxDy = maxDy * pixelsToDraw / mPixelArea;
   711             // and finally generate the displayport. the min/max stuff takes care of
   712             // negative velocities as well as positive.
   713             RectF margins = new RectF(
   714                 -Math.min(minDx, maxDx),
   715                 -Math.min(minDy, maxDy),
   716                 Math.max(minDx, maxDx),
   717                 Math.max(minDy, maxDy));
   718             return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
   719         }
   721         @Override
   722         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
   723             // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
   724             // refer to the comments in calculate() to understand what this is doing.
   725             float minDx = velocity.x * mMinFramesToDraw;
   726             float minDy = velocity.y * mMinFramesToDraw;
   727             float maxDx = velocity.x * mMaxFramesToDraw;
   728             float maxDy = velocity.y * mMaxFramesToDraw;
   729             float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy));
   730             maxDx = maxDx * pixelsToDraw / mPixelArea;
   731             maxDy = maxDy * pixelsToDraw / mPixelArea;
   733             // now that we have an idea of how far we will be when the draw completes, take the farthest
   734             // end of that range and see if it falls outside the displayport bounds. if it does, allow
   735             // the draw to go through
   736             RectF predictedViewport = metrics.getViewport();
   737             predictedViewport.left += maxDx;
   738             predictedViewport.top += maxDy;
   739             predictedViewport.right += maxDx;
   740             predictedViewport.bottom += maxDy;
   742             predictedViewport = clampToPageBounds(predictedViewport, metrics);
   743             return !displayPort.contains(predictedViewport);
   744         }
   746         @Override
   747         public boolean drawTimeUpdate(long millis, int pixels) {
   748             // calculate the number of frames it took to draw a viewport-sized area
   749             float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels;
   750             int normalizedFrames = (int)FloatMath.ceil(normalizedTime * 60f / 1000f);
   751             // broaden our range on how long it takes to draw if the draw falls outside
   752             // the range. this allows it to grow gradually. this heuristic may need to
   753             // be tweaked into more of a floating window average or something.
   754             if (normalizedFrames <= mMinFramesToDraw) {
   755                 mMinFramesToDraw--;
   756             } else if (normalizedFrames > mMaxFramesToDraw) {
   757                 mMaxFramesToDraw++;
   758             } else {
   759                 return true;
   760             }
   761             Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]");
   762             return true;
   763         }
   765         @Override
   766         public void resetPageState() {
   767             mMinFramesToDraw = 0;
   768             mMaxFramesToDraw = 2;
   769         }
   771         @Override
   772         public String toString() {
   773             return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
   774         }
   775     }
   776 }

mercurial