mobile/android/base/gfx/DisplayPortCalculator.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:30096b801627
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/. */
5
6 package org.mozilla.gecko.gfx;
7
8 import org.mozilla.gecko.GeckoAppShell;
9 import org.mozilla.gecko.PrefsHelper;
10 import org.mozilla.gecko.util.FloatUtils;
11
12 import org.json.JSONArray;
13
14 import android.graphics.PointF;
15 import android.graphics.RectF;
16 import android.util.FloatMath;
17 import android.util.Log;
18
19 import java.util.HashMap;
20 import java.util.Map;
21
22 final class DisplayPortCalculator {
23 private static final String LOGTAG = "GeckoDisplayPort";
24 private static final PointF ZERO_VELOCITY = new PointF(0, 0);
25
26 // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h
27 private static final int TILE_SIZE = 256;
28
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";
41
42 private static DisplayPortStrategy sStrategy = new VelocityBiasStrategy(null);
43
44 static DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
45 return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity));
46 }
47
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 }
54
55 static boolean drawTimeUpdate(long millis, int pixels) {
56 return sStrategy.drawTimeUpdate(millis, pixels);
57 }
58
59 static void resetPageState() {
60 sStrategy.resetPageState();
61 }
62
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 };
76
77 PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
78 private Map<String, Integer> mValues = new HashMap<String, Integer>();
79
80 @Override public void prefValue(String pref, int value) {
81 mValues.put(pref, value);
82 }
83
84 @Override public void finish() {
85 setStrategy(mValues);
86 }
87 });
88 }
89
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 }
100
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 }
124
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 }
129
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 }
140
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 }
161
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 }
175
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 }
193
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;
208
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 }
226
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 }
237
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 }
245
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 }
254
255 @Override
256 public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
257 return true;
258 }
259
260 @Override
261 public String toString() {
262 return "NoMarginStrategy";
263 }
264 }
265
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;
278
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;
283
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 }
289
290 @Override
291 public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
292 float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
293 float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
294
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();
302
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);
314
315 return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
316 }
317
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 }
326
327 @Override
328 public String toString() {
329 return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER;
330 }
331 }
332
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;
358
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 }
368
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();
379
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;
388
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;
397
398 return margins;
399 }
400
401 @Override
402 public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
403 float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
404 float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
405
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 }
413
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();
420
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);
425
426 return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
427 }
428
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());
438
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);
443
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 }
453
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 }
461
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;
474
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;
478
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;
496
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;
506
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)
521
522 DynamicResolutionStrategy(Map<String, Integer> prefs) {
523 // ignore prefs for now
524 }
525
526 @Override
527 public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
528 float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER;
529 float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER;
530
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);
535
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;
542
543 displayPortWidth += (displayPortWidth * velocityFactor);
544 displayPortHeight += (displayPortHeight * velocityFactor);
545 }
546
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
550
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();
560
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;
571
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);
575
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);
588
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 }
597
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 }
625
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.
631
632 RectF predictedViewport = metrics.getViewport();
633
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 }
642
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 }
649
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 }
655
656 @Override
657 public String toString() {
658 return "DynamicResolutionStrategy";
659 }
660 }
661
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;
674
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
678
679 PredictionBiasStrategy(Map<String, Integer> prefs) {
680 VELOCITY_THRESHOLD = GeckoAppShell.getDpi() * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16);
681 resetPageState();
682 }
683
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);
689
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 }
695
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;
701
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;
710
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 }
720
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;
732
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;
741
742 predictedViewport = clampToPageBounds(predictedViewport, metrics);
743 return !displayPort.contains(predictedViewport);
744 }
745
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 }
764
765 @Override
766 public void resetPageState() {
767 mMinFramesToDraw = 0;
768 mMaxFramesToDraw = 2;
769 }
770
771 @Override
772 public String toString() {
773 return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD;
774 }
775 }
776 }

mercurial