|
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 } |