Bug 786267: B2G: Lower resolution while doing accelerated panning r=cjones
authorDoug Sherk <dsherk2@mozilla.com>
Sat, 29 Sep 2012 00:02:45 -0400
changeset 112196 33031c64f7d16d3b5fafa71ef6cbc857aeeb2482
parent 112195 9698936945c6159814a7246231856fb47cc20102
child 112197 b4d33b66bfba37f5d1bfd79b9bef15c314df2eeb
push idunknown
push userunknown
push dateunknown
reviewerscjones
bugs786267
milestone18.0a1
Bug 786267: B2G: Lower resolution while doing accelerated panning r=cjones
dom/ipc/TabChild.cpp
gfx/layers/ipc/AsyncPanZoomController.cpp
gfx/layers/ipc/AsyncPanZoomController.h
gfx/layers/ipc/Axis.cpp
gfx/layers/ipc/Axis.h
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -454,18 +454,20 @@ TabChild::HandlePossibleMetaViewportChan
 
   FrameMetrics metrics(mLastMetrics);
   metrics.mViewport = gfx::Rect(0.0f, 0.0f, viewportW, viewportH);
   metrics.mScrollableRect = gfx::Rect(0.0f, 0.0f, pageWidth, pageHeight);
   metrics.mCompositionBounds = nsIntRect(0, 0, mInnerSize.width, mInnerSize.height);
   metrics.mZoom.width = metrics.mZoom.height =
     metrics.mResolution.width = metrics.mResolution.height = zoom;
   metrics.mDisplayPort = AsyncPanZoomController::CalculatePendingDisplayPort(
-    metrics,
-    gfx::Point(0.0f, 0.0f));
+    // The page must have been refreshed in some way such as a new document or
+    // new CSS viewport, so we know that there's no velocity, acceleration, and
+    // we have no idea how long painting will take.
+    metrics, gfx::Point(0.0f, 0.0f), gfx::Point(0.0f, 0.0f), 0.0);
   // Force a repaint with these metrics. This, among other things, sets the
   // displayport, so we start with async painting.
   RecvUpdateFrame(metrics);
 }
 
 nsresult
 TabChild::Init()
 {
--- a/gfx/layers/ipc/AsyncPanZoomController.cpp
+++ b/gfx/layers/ipc/AsyncPanZoomController.cpp
@@ -39,17 +39,17 @@ static const int32_t PAN_REPAINT_INTERVA
  * asynchronously repaint the page.
  */
 static const int32_t FLING_REPAINT_INTERVAL = 75;
 
 /**
  * Minimum amount of speed along an axis before we begin painting far ahead by
  * adjusting the displayport.
  */
-static const float MIN_SKATE_SPEED = 0.5f;
+static const float MIN_SKATE_SPEED = 0.7f;
 
 /**
  * Angle from axis within which we stay axis-locked.
  */
 static const float AXIS_LOCK_ANGLE = M_PI / 9.0;
 
 /**
  * Duration of a zoom to animation.
@@ -74,16 +74,22 @@ static const double MIN_ZOOM = 0.125;
 /**
  * Amount of time before we timeout touch event listeners. For example, if
  * content is being unruly/slow and we don't get a response back within this
  * time, we will just pretend that content did not preventDefault any touch
  * events we dispatched to it.
  */
 static const int TOUCH_LISTENER_TIMEOUT = 300;
 
+/**
+ * Number of samples to store of how long it took to paint after the previous
+ * requests.
+ */
+static const int NUM_PAINT_DURATION_SAMPLES = 3;
+
 AsyncPanZoomController::AsyncPanZoomController(GeckoContentController* aGeckoContentController,
                                                GestureBehavior aGestures)
   :  mGeckoContentController(aGeckoContentController),
      mTouchListenerTimeoutTask(nullptr),
      mX(this),
      mY(this),
      mAllowZoom(true),
      mMinZoom(MIN_ZOOM),
@@ -582,16 +588,20 @@ float AsyncPanZoomController::PanDistanc
   MonitorAutoLock monitor(mMonitor);
   return NS_hypot(mX.PanDistance(), mY.PanDistance());
 }
 
 const gfx::Point AsyncPanZoomController::GetVelocityVector() {
   return gfx::Point(mX.GetVelocity(), mY.GetVelocity());
 }
 
+const gfx::Point AsyncPanZoomController::GetAccelerationVector() {
+  return gfx::Point(mX.GetAccelerationFactor(), mY.GetAccelerationFactor());
+}
+
 void AsyncPanZoomController::StartPanning(const MultiTouchInput& aEvent) {
   float dx = mX.PanDistance(),
         dy = mY.PanDistance();
 
   double angle = atan2(dy, dx); // range [-pi, pi]
   angle = fabs(angle); // range [0, pi]
 
   SetState(PANNING);
@@ -656,16 +666,19 @@ bool AsyncPanZoomController::DoFling(con
   if (mState != FLING) {
     return false;
   }
 
   bool shouldContinueFlingX = mX.FlingApplyFrictionOrCancel(aDelta),
        shouldContinueFlingY = mY.FlingApplyFrictionOrCancel(aDelta);
   // If we shouldn't continue the fling, let's just stop and repaint.
   if (!shouldContinueFlingX && !shouldContinueFlingY) {
+    // Bring the resolution back in sync with the zoom, in case we scaled down
+    // the zoom while accelerating.
+    SetZoomAndResolution(mFrameMetrics.mZoom.width);
     RequestContentRepaint();
     mState = NOTHING;
     return false;
   }
 
   // We want to inversely scale it because when you're zoomed further in, a
   // larger swipe should move you a shorter distance.
   float inverseScale = 1 / mFrameMetrics.mZoom.width;
@@ -707,78 +720,147 @@ void AsyncPanZoomController::SetPageRect
   // from this.
   metrics.mContentRect = nsIntRect(pageSize.x, pageSize.y, pageSize.width, pageSize.height);
   metrics.mScrollableRect = aCSSPageRect;
 
   mFrameMetrics = metrics;
 }
 
 void AsyncPanZoomController::ScaleWithFocus(float aScale, const nsIntPoint& aFocus) {
-  float scaleFactor = aScale / mFrameMetrics.mZoom.width,
-        oldScale = mFrameMetrics.mZoom.width;
+  float scaleFactor = aScale / mFrameMetrics.mZoom.width;
 
   SetZoomAndResolution(aScale);
 
   // Force a recalculation of the page rect based on the new zoom and the
   // current CSS page rect (which is unchanged since it's not affected by zoom).
   SetPageRect(mFrameMetrics.mScrollableRect);
 
-  mFrameMetrics.mScrollOffset.x += float(aFocus.x) * (scaleFactor - 1.0f) / oldScale;
-  mFrameMetrics.mScrollOffset.y += float(aFocus.y) * (scaleFactor - 1.0f) / oldScale;
+  // If the new scale is very small, we risk multiplying in huge rounding
+  // errors, so don't bother adjusting the scroll offset.
+  if (aScale >= 0.01f) {
+    mFrameMetrics.mScrollOffset.x += float(aFocus.x) * (scaleFactor - 1.0f) / aScale;
+    mFrameMetrics.mScrollOffset.y += float(aFocus.y) * (scaleFactor - 1.0f) / aScale;
+  }
 }
 
-bool AsyncPanZoomController::EnlargeDisplayPortAlongAxis(float aCompositionBounds,
+bool AsyncPanZoomController::EnlargeDisplayPortAlongAxis(float aSkateSizeMultiplier,
+                                                         double aEstimatedPaintDuration,
+                                                         float aCompositionBounds,
                                                          float aVelocity,
+                                                         float aAcceleration,
                                                          float* aDisplayPortOffset,
                                                          float* aDisplayPortLength)
 {
-  const float MIN_SKATE_SIZE_MULTIPLIER = 2.0f;
-  const float MAX_SKATE_SIZE_MULTIPLIER = 4.0f;
+  if (fabsf(aVelocity) > MIN_SKATE_SPEED) {
+    // Enlarge the area we paint.
+    *aDisplayPortLength = aCompositionBounds * aSkateSizeMultiplier;
+    // Position the area we paint such that all of the excess that extends past
+    // the screen is on the side towards the velocity.
+    *aDisplayPortOffset = aVelocity > 0 ? 0 : aCompositionBounds - *aDisplayPortLength;
 
-  if (fabsf(aVelocity) > MIN_SKATE_SPEED) {
-    *aDisplayPortLength = aCompositionBounds * clamped(fabsf(aVelocity),
-      MIN_SKATE_SIZE_MULTIPLIER, MAX_SKATE_SIZE_MULTIPLIER);
-    *aDisplayPortOffset = aVelocity > 0 ? 0 : aCompositionBounds - *aDisplayPortLength;
+    // Only compensate for acceleration when we actually have any. Otherwise
+    // we'll overcompensate when a user is just panning around without flinging.
+    if (aAcceleration > 1.01f) {
+      // Compensate for acceleration and how long we expect a paint to take. We
+      // try to predict where the viewport will be when painting has finished.
+      *aDisplayPortOffset +=
+        fabsf(aAcceleration) * aVelocity * aCompositionBounds * aEstimatedPaintDuration;
+      // If our velocity is in the negative direction of the axis, we have to
+      // compensate for the fact that our scroll offset is the top-left position
+      // of the viewport. In this case, let's make it relative to the
+      // bottom-right. That way, we'll always be growing the displayport upwards
+      // and to the left when skating negatively.
+      *aDisplayPortOffset -= aVelocity < 0 ? aCompositionBounds : 0;
+    }
     return true;
   }
   return false;
 }
 
 const gfx::Rect AsyncPanZoomController::CalculatePendingDisplayPort(
   const FrameMetrics& aFrameMetrics,
-  const gfx::Point& aVelocity)
+  const gfx::Point& aVelocity,
+  const gfx::Point& aAcceleration,
+  double aEstimatedPaintDuration)
 {
+  // The multiplier we apply to a dimension's length if it is skating. That is,
+  // if it's going above MIN_SKATE_SPEED. We prefer to increase the size of the
+  // Y axis because it is more natural in the case that a user is reading a page
+  // that scrolls up/down. Note that one, both or neither of these may be used
+  // at any instant.
+  const float X_SKATE_SIZE_MULTIPLIER = 3.0f;
+  const float Y_SKATE_SIZE_MULTIPLIER = 3.5f;
+
+  // The multiplier we apply to a dimension's length if it is stationary. We
+  // prefer to increase the size of the Y axis because it is more natural in the
+  // case that a user is reading a page that scrolls up/down. Note that one,
+  // both or neither of these may be used at any instant.
+  const float X_STATIONARY_SIZE_MULTIPLIER = 1.5f;
+  const float Y_STATIONARY_SIZE_MULTIPLIER = 2.5f;
+
+  // If we don't get an estimated paint duration, we probably don't have any
+  // data. In this case, we're dealing with either a stationary frame or a first
+  // paint. In either of these cases, we can just assume it'll take 1 second to
+  // paint. Getting this correct is not important anyways since it's only really
+  // useful when accelerating, which can't be happening at this point.
+  double estimatedPaintDuration =
+    aEstimatedPaintDuration > EPSILON ? aEstimatedPaintDuration : 1.0;
+
   float scale = aFrameMetrics.mZoom.width;
   nsIntRect compositionBounds = aFrameMetrics.mCompositionBounds;
   compositionBounds.ScaleInverseRoundIn(scale);
+  const gfx::Rect& scrollableRect = aFrameMetrics.mScrollableRect;
 
   gfx::Point scrollOffset = aFrameMetrics.mScrollOffset;
 
-  const float STATIONARY_SIZE_MULTIPLIER = 2.0f;
   gfx::Rect displayPort(0, 0,
-                        compositionBounds.width * STATIONARY_SIZE_MULTIPLIER,
-                        compositionBounds.height * STATIONARY_SIZE_MULTIPLIER);
+                        compositionBounds.width * X_STATIONARY_SIZE_MULTIPLIER,
+                        compositionBounds.height * Y_STATIONARY_SIZE_MULTIPLIER);
 
   // If there's motion along an axis of movement, and it's above a threshold,
   // then we want to paint a larger area in the direction of that motion so that
   // it's less likely to checkerboard.
   bool enlargedX = EnlargeDisplayPortAlongAxis(
-    compositionBounds.width, aVelocity.x, &displayPort.x, &displayPort.width);
+    X_SKATE_SIZE_MULTIPLIER, estimatedPaintDuration,
+    compositionBounds.width, aVelocity.x, aAcceleration.x,
+    &displayPort.x, &displayPort.width);
   bool enlargedY = EnlargeDisplayPortAlongAxis(
-    compositionBounds.height, aVelocity.y, &displayPort.y, &displayPort.height);
+    Y_SKATE_SIZE_MULTIPLIER, estimatedPaintDuration,
+    compositionBounds.height, aVelocity.y, aAcceleration.y,
+    &displayPort.y, &displayPort.height);
 
   if (!enlargedX && !enlargedY) {
-    displayPort.x = -displayPort.width / 4;
-    displayPort.y = -displayPort.height / 4;
+    // Position the x and y such that the screen falls in the middle of the displayport.
+    displayPort.x = -(displayPort.width - compositionBounds.width) / 2;
+    displayPort.y = -(displayPort.height - compositionBounds.height) / 2;
   } else if (!enlargedX) {
     displayPort.width = compositionBounds.width;
   } else if (!enlargedY) {
     displayPort.height = compositionBounds.height;
   }
 
+  // If we go over the bounds when trying to predict where we will be when this
+  // paint finishes, move it back into the range of the CSS content rect.
+  // FIXME/bug 780395: Generalize this. This code is pretty hacky as it will
+  // probably not work at all for RTL content. This is not intended to be
+  // incredibly accurate; it'll just prevent the entire displayport from being
+  // outside the content rect (which causes bad things to happen).
+  if (enlargedX || enlargedY) {
+    if (scrollOffset.x + compositionBounds.width > scrollableRect.width) {
+      scrollOffset.x -= compositionBounds.width + scrollOffset.x - scrollableRect.width;
+    } else if (scrollOffset.x < scrollableRect.x) {
+      scrollOffset.x = scrollableRect.x;
+    }
+    if (scrollOffset.y + compositionBounds.height > scrollableRect.height) {
+      scrollOffset.y -= compositionBounds.height + scrollOffset.y - scrollableRect.height;
+    } else if (scrollOffset.y < scrollableRect.y) {
+      scrollOffset.y = scrollableRect.y;
+    }
+  }
+
   gfx::Rect shiftedDisplayPort = displayPort;
   shiftedDisplayPort.MoveBy(scrollOffset.x, scrollOffset.y);
   displayPort = shiftedDisplayPort.Intersect(aFrameMetrics.mScrollableRect);
   displayPort.MoveBy(-scrollOffset.x, -scrollOffset.y);
 
   return displayPort;
 }
 
@@ -792,18 +874,33 @@ int AsyncPanZoomController::GetDPI() {
 
 void AsyncPanZoomController::ScheduleComposite() {
   if (mCompositorParent) {
     mCompositorParent->ScheduleRenderOnCompositorThread();
   }
 }
 
 void AsyncPanZoomController::RequestContentRepaint() {
+  mPreviousPaintStartTime = TimeStamp::Now();
+
+  double estimatedPaintSum = 0.0;
+  for (uint32_t i = 0; i < mPreviousPaintDurations.Length(); i++) {
+    estimatedPaintSum += mPreviousPaintDurations[i].ToSeconds();
+  }
+
+  double estimatedPaintDuration = 0.0;
+  if (estimatedPaintSum > EPSILON) {
+    estimatedPaintDuration = estimatedPaintSum / mPreviousPaintDurations.Length();
+  }
+
   mFrameMetrics.mDisplayPort =
-    CalculatePendingDisplayPort(mFrameMetrics, GetVelocityVector());
+    CalculatePendingDisplayPort(mFrameMetrics,
+                                GetVelocityVector(),
+                                GetAccelerationVector(),
+                                estimatedPaintDuration);
 
   gfx::Point oldScrollOffset = mLastPaintRequestMetrics.mScrollOffset,
              newScrollOffset = mFrameMetrics.mScrollOffset;
 
   // If we're trying to paint what we already think is painted, discard this
   // request since it's a pointless paint.
   gfx::Rect oldDisplayPort = mLastPaintRequestMetrics.mDisplayPort;
   gfx::Rect newDisplayPort = mFrameMetrics.mDisplayPort;
@@ -814,22 +911,37 @@ void AsyncPanZoomController::RequestCont
   if (fabsf(oldDisplayPort.x - newDisplayPort.x) < EPSILON &&
       fabsf(oldDisplayPort.y - newDisplayPort.y) < EPSILON &&
       fabsf(oldDisplayPort.width - newDisplayPort.width) < EPSILON &&
       fabsf(oldDisplayPort.height - newDisplayPort.height) < EPSILON &&
       mFrameMetrics.mResolution.width == mLastPaintRequestMetrics.mResolution.width) {
     return;
   }
 
+  // Cache the resolution since we're temporarily changing it to accomodate
+  // mixed resolution/zoom (normally we make them the same thing).
+  float actualResolution = mFrameMetrics.mResolution.width;
+  // Calculate the factor of acceleration based on the faster of the two axes.
+  float accelerationFactor =
+    clamped(NS_MAX(mX.GetAccelerationFactor(), mY.GetAccelerationFactor()),
+            float(MIN_ZOOM) / 2.0f, float(MAX_ZOOM));
+  // Scale down the resolution a bit based on acceleration.
+  mFrameMetrics.mResolution.width = mFrameMetrics.mResolution.height =
+    actualResolution / accelerationFactor;
+
   // This message is compressed, so fire whether or not we already have a paint
   // queued up. We need to know whether or not a paint was requested anyways,
   // ofr the purposes of content calling window.scrollTo().
   mGeckoContentController->RequestContentRepaint(mFrameMetrics);
   mLastPaintRequestMetrics = mFrameMetrics;
   mWaitingForContentToPaint = true;
+
+  // Set the resolution back to what it was for the purpose of logic control.
+  mFrameMetrics.mResolution.width = mFrameMetrics.mResolution.height =
+    actualResolution;
 }
 
 bool AsyncPanZoomController::SampleContentTransformForFrame(const TimeStamp& aSampleTime,
                                                             ContainerLayer* aLayer,
                                                             gfx3DMatrix* aNewTransform) {
   // The eventual return value of this function. The compositor needs to know
   // whether or not to advance by a frame as soon as it can. For example, if a
   // fling is happening, it has to keep compositing so that the animation is
@@ -926,17 +1038,26 @@ bool AsyncPanZoomController::SampleConte
   return requestAnimationFrame;
 }
 
 void AsyncPanZoomController::NotifyLayersUpdated(const FrameMetrics& aViewportFrame, bool aIsFirstPaint) {
   MonitorAutoLock monitor(mMonitor);
 
   mLastContentPaintMetrics = aViewportFrame;
 
-  if (!mWaitingForContentToPaint) {
+  if (mWaitingForContentToPaint) {
+    // Remove the oldest sample we have if adding a new sample takes us over our
+    // desired number of samples.
+    if (mPreviousPaintDurations.Length() >= NUM_PAINT_DURATION_SAMPLES) {
+      mPreviousPaintDurations.RemoveElementAt(0);
+    }
+
+    mPreviousPaintDurations.AppendElement(
+      TimeStamp::Now() - mPreviousPaintStartTime);
+  } else {
     // No paint was requested, but we got one anyways. One possible cause of this
     // is that content could have fired a scrollTo(). In this case, we should take
     // the new scroll offset. Document/viewport changes are handled elsewhere.
     // Also note that, since NotifyLayersUpdated() is called whenever there's a
     // layers update, we didn't necessarily get a new scroll offset, but we're
     // updating our local copy of it anyways just in case.
     switch (mState) {
     case NOTHING:
@@ -949,16 +1070,18 @@ void AsyncPanZoomController::NotifyLayer
     default:
       break;
     }
   }
 
   mWaitingForContentToPaint = false;
 
   if (aIsFirstPaint || mFrameMetrics.IsDefault()) {
+    mPreviousPaintDurations.Clear();
+
     mX.CancelTouch();
     mY.CancelTouch();
 
     // The composition bounds are not stored within the layers code, so we have
     // to reset them back to what they were every time we overwrite them.
     nsIntRect compositionBounds = mFrameMetrics.mCompositionBounds;
     mFrameMetrics = aViewportFrame;
     mFrameMetrics.mCompositionBounds = compositionBounds;
--- a/gfx/layers/ipc/AsyncPanZoomController.h
+++ b/gfx/layers/ipc/AsyncPanZoomController.h
@@ -200,17 +200,19 @@ public:
   /**
    * Recalculates the displayport. Ideally, this should paint an area bigger
    * than the composite-to dimensions so that when you scroll down, you don't
    * checkerboard immediately. This includes a bunch of logic, including
    * algorithms to bias painting in the direction of the velocity.
    */
   static const gfx::Rect CalculatePendingDisplayPort(
     const FrameMetrics& aFrameMetrics,
-    const gfx::Point& aVelocity);
+    const gfx::Point& aVelocity,
+    const gfx::Point& aAcceleration,
+    double aEstimatedPaintDuration);
 
 protected:
   /**
    * Internal handler for ReceiveInputEvent(). Does all the actual work.
    */
   nsEventStatus HandleInputEvent(const InputData& aEvent);
 
   /**
@@ -329,16 +331,21 @@ protected:
   float PanDistance();
 
   /**
    * Gets a vector of the velocities of each axis.
    */
   const gfx::Point GetVelocityVector();
 
   /**
+   * Gets a vector of the acceleration factors of each axis.
+   */
+  const gfx::Point GetAccelerationVector();
+
+  /**
    * Gets a reference to the first SingleTouchData from a MultiTouchInput.  This
    * gets only the first one and assumes the rest are either missing or not
    * relevant.
    */
   SingleTouchData& GetFirstSingleTouch(const MultiTouchInput& aEvent);
 
   /**
    * Sets up anything needed for panning. This may lock one of the axes if the
@@ -360,18 +367,21 @@ protected:
   /**
    * Attempts to enlarge the displayport along a single axis. Returns whether or
    * not the displayport was enlarged. This will fail in circumstances where the
    * velocity along that axis is not high enough to need any changes. The
    * displayport metrics are expected to be passed into |aDisplayPortOffset| and
    * |aDisplayPortLength|. If enlarged, these will be updated with the new
    * metrics.
    */
-  static bool EnlargeDisplayPortAlongAxis(float aCompositionBounds,
+  static bool EnlargeDisplayPortAlongAxis(float aSkateSizeMultiplier,
+                                          double aEstimatedPaintDuration,
+                                          float aCompositionBounds,
                                           float aVelocity,
+                                          float aAcceleration,
                                           float* aDisplayPortOffset,
                                           float* aDisplayPortLength);
 
   /**
    * Utility function to send updated FrameMetrics to Gecko so that it can paint
    * the displayport area. Calls into GeckoContentController to do the actual
    * work. Note that only one paint request can be active at a time. If a paint
    * request is made while a paint is currently happening, it gets queued up. If
@@ -491,16 +501,23 @@ private:
   // Stores the previous focus point if there is a pinch gesture happening. Used
   // to allow panning by moving multiple fingers (thus moving the focus point).
   nsIntPoint mLastZoomFocus;
 
   // Stores the state of panning and zooming this frame. This is protected by
   // |mMonitor|; that is, it should be held whenever this is updated.
   PanZoomState mState;
 
+  // How long it took in the past to paint after a series of previous requests.
+  nsTArray<TimeDuration> mPreviousPaintDurations;
+
+  // When the last paint request started. Used to determine the duration of
+  // previous paints.
+  TimeStamp mPreviousPaintStartTime;
+
   int mDPI;
 
   // Stores the current paint status of the frame that we're managing. Repaints
   // may be triggered by other things (like content doing things), in which case
   // this status will not be updated. It is only changed when this class
   // requests a repaint.
   bool mWaitingForContentToPaint;
 
--- a/gfx/layers/ipc/Axis.cpp
+++ b/gfx/layers/ipc/Axis.cpp
@@ -13,17 +13,17 @@ namespace layers {
 static const float EPSILON = 0.0001f;
 
 /**
  * Maximum acceleration that can happen between two frames. Velocity is
  * throttled if it's above this. This may happen if a time delta is very low,
  * or we get a touch point very far away from the previous position for some
  * reason.
  */
-static const float MAX_EVENT_ACCELERATION = 0.5f;
+static const float MAX_EVENT_ACCELERATION = 999.0f;
 
 /**
  * Amount of friction applied during flings.
  */
 static const float FLING_FRICTION = 0.007f;
 
 /**
  * Threshold for velocity beneath which we turn off any acceleration we had
@@ -90,25 +90,29 @@ void Axis::UpdateWithTouchAtDevicePoint(
 
 void Axis::StartTouch(int32_t aPos) {
   mStartPos = aPos;
   mPos = aPos;
   mLockPanning = false;
 }
 
 float Axis::GetDisplacementForDuration(float aScale, const TimeDuration& aDelta) {
-  float velocityFactor = powf(ACCELERATION_MULTIPLIER,
-                              NS_MAX(0, (mAcceleration - 4) * 3));
-  float displacement = mVelocity * aScale * aDelta.ToMilliseconds() * velocityFactor;
+  if (fabsf(mVelocity) < VELOCITY_THRESHOLD) {
+    mAcceleration = 0;
+  }
+
+  float accelerationFactor = GetAccelerationFactor();
+  float displacement = mVelocity * aScale * aDelta.ToMilliseconds() * accelerationFactor;
   // If this displacement will cause an overscroll, throttle it. Can potentially
   // bring it to 0 even if the velocity is high.
   if (DisplacementWillOverscroll(displacement) != OVERSCROLL_NONE) {
     // No need to have a velocity along this axis anymore; it won't take us
     // anywhere, so we're just spinning needlessly.
     mVelocity = 0.0f;
+    mAcceleration = 0;
     displacement -= DisplacementWillOverscrollAmount(displacement);
   }
   return displacement;
 }
 
 float Axis::PanDistance() {
   return fabsf(mPos - mStartPos);
 }
@@ -226,16 +230,20 @@ float Axis::ScaleWillOverscrollAmount(fl
   default: return 0;
   }
 }
 
 float Axis::GetVelocity() {
   return mVelocity;
 }
 
+float Axis::GetAccelerationFactor() {
+  return powf(ACCELERATION_MULTIPLIER, NS_MAX(0, (mAcceleration - 4) * 3));
+}
+
 float Axis::GetCompositionEnd() {
   return GetOrigin() + GetCompositionLength();
 }
 
 float Axis::GetPageEnd() {
   return GetPageStart() + GetPageLength();
 }
 
--- a/gfx/layers/ipc/Axis.h
+++ b/gfx/layers/ipc/Axis.h
@@ -112,16 +112,22 @@ public:
    * overscrolling in the positive direction, whereas negative excess means
    * that it is overscrolling in the negative direction. If there is overscroll
    * in both directions, this returns 0; it assumes that you check
    * GetOverscroll() first.
    */
   float GetExcess();
 
   /**
+   * Gets the factor of acceleration applied to the velocity, based on the
+   * amount of flings that have been done successively.
+   */
+  float GetAccelerationFactor();
+
+  /**
    * Gets the raw velocity of this axis at this moment.
    */
   float GetVelocity();
 
   /**
    * Gets the overscroll state of the axis given an additional displacement.
    * That is to say, if the given displacement is applied, this will tell you
    * whether or not it will overscroll, and in what direction.