Bug 1457586 - Introduce a VelocityTracker abstraction for handling velocity computation along an axis. r=kats draft
authorBotond Ballo <botond@mozilla.com>
Mon, 23 Jul 2018 12:05:42 -0400
changeset 827066 d55aa360ae5c09b793f858f3436aeb10cc465c2e
parent 826876 5d470348b10dba774ab9933a10858a2f1bee1a72
child 827067 8d21aa3841f9f8ddf4f4f309022a81971476ec35
push id118454
push userbballo@mozilla.com
push dateMon, 06 Aug 2018 22:41:31 +0000
reviewerskats
bugs1457586
milestone63.0a1
Bug 1457586 - Introduce a VelocityTracker abstraction for handling velocity computation along an axis. r=kats The current velocity computation code is factored out into an implementation called SimpleVelocityTracker. MozReview-Commit-ID: G0VnvREdIX3
gfx/layers/apz/src/Axis.cpp
gfx/layers/apz/src/Axis.h
gfx/layers/apz/src/SimpleVelocityTracker.cpp
gfx/layers/apz/src/SimpleVelocityTracker.h
gfx/layers/moz.build
--- a/gfx/layers/apz/src/Axis.cpp
+++ b/gfx/layers/apz/src/Axis.cpp
@@ -6,168 +6,92 @@
 
 #include "Axis.h"
 
 #include <math.h>                       // for fabsf, pow, powf
 #include <algorithm>                    // for max
 
 #include "APZCTreeManager.h"            // for APZCTreeManager
 #include "AsyncPanZoomController.h"     // for AsyncPanZoomController
-#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread
 #include "FrameMetrics.h"               // for FrameMetrics
+#include "SimpleVelocityTracker.h"      // for FrameMetrics
 #include "mozilla/Attributes.h"         // for final
-#include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
 #include "mozilla/Preferences.h"        // for Preferences
 #include "mozilla/gfx/Rect.h"           // for RoundedIn
+#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread
 #include "mozilla/mozalloc.h"           // for operator new
 #include "mozilla/FloatingPoint.h"      // for FuzzyEqualsAdditive
-#include "mozilla/StaticPtr.h"          // for StaticAutoPtr
 #include "nsMathUtils.h"                // for NS_lround
 #include "nsPrintfCString.h"            // for nsPrintfCString
 #include "nsThreadUtils.h"              // for NS_DispatchToMainThread, etc
 #include "nscore.h"                     // for NS_IMETHOD
 #include "gfxPrefs.h"                   // for the preferences
 
 #define AXIS_LOG(...)
 // #define AXIS_LOG(...) printf_stderr("AXIS: " __VA_ARGS__)
 
 namespace mozilla {
 namespace layers {
 
-// When we compute the velocity we do so by taking two input events and
-// dividing the distance delta over the time delta. In some cases the time
-// delta can be really small, which can make the velocity computation very
-// volatile. To avoid this we impose a minimum time delta below which we do
-// not recompute the velocity.
-const uint32_t MIN_VELOCITY_SAMPLE_TIME_MS = 5;
-
 bool FuzzyEqualsCoordinate(float aValue1, float aValue2)
 {
   return FuzzyEqualsAdditive(aValue1, aValue2, COORDINATE_EPSILON)
       || FuzzyEqualsMultiplicative(aValue1, aValue2);
 }
 
-extern StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
-
 Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController)
   : mPos(0),
-    mVelocitySampleTimeMs(0),
-    mVelocitySamplePos(0),
     mVelocity(0.0f),
     mAxisLocked(false),
     mAsyncPanZoomController(aAsyncPanZoomController),
     mOverscroll(0),
-    mMSDModel(0.0, 0.0, 0.0, 400.0, 1.2)
+    mMSDModel(0.0, 0.0, 0.0, 400.0, 1.2),
+    mVelocityTracker(MakeUnique<SimpleVelocityTracker>(this))
 {
 }
 
 float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const {
   ScreenPoint velocity = MakePoint(aVelocityInchesPerMs * mAsyncPanZoomController->GetDPI());
   // Use ToScreenCoordinates() to convert a point rather than a vector by
   // treating the point as a vector, and using (0, 0) as the anchor.
   ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates(
       mAsyncPanZoomController->PanStart(),
       ParentLayerPoint());
   ParentLayerPoint localVelocity =
       mAsyncPanZoomController->ToParentLayerCoordinates(velocity, panStart);
   return localVelocity.Length();
 }
 
 void Axis::UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, uint32_t aTimestampMs) {
-  // mVelocityQueue is controller-thread only
+  // mVelocityTracker is controller-thread only
   APZThreadUtils::AssertOnControllerThread();
 
-  if (aTimestampMs <= mVelocitySampleTimeMs + MIN_VELOCITY_SAMPLE_TIME_MS) {
-    // See also the comment on MIN_VELOCITY_SAMPLE_TIME_MS.
-    // We still update mPos so that the positioning is correct (and we don't run
-    // into problems like bug 1042734) but the velocity will remain where it was.
-    // In particular we don't update either mVelocitySampleTimeMs or
-    // mVelocitySamplePos so that eventually when we do get an event with the
-    // required time delta we use the corresponding distance delta as well.
-    AXIS_LOG("%p|%s skipping velocity computation for small time delta %dms\n",
-        mAsyncPanZoomController, Name(), (aTimestampMs - mVelocitySampleTimeMs));
-    mPos = aPos;
-    return;
-  }
-
-  float newVelocity = mAxisLocked ? 0.0f : (float)(mVelocitySamplePos - aPos) / (float)(aTimestampMs - mVelocitySampleTimeMs);
-
-  newVelocity = ApplyFlingCurveToVelocity(newVelocity);
-
-  AXIS_LOG("%p|%s updating velocity to %f with touch\n",
-    mAsyncPanZoomController, Name(), newVelocity);
-  mVelocity = newVelocity;
   mPos = aPos;
-  mVelocitySampleTimeMs = aTimestampMs;
-  mVelocitySamplePos = aPos;
-
-  AddVelocityToQueue(aTimestampMs, mVelocity);
-}
 
-float Axis::ApplyFlingCurveToVelocity(float aVelocity) const {
-  float newVelocity = aVelocity;
-  if (gfxPrefs::APZMaxVelocity() > 0.0f) {
-    bool velocityIsNegative = (newVelocity < 0);
-    newVelocity = fabs(newVelocity);
-
-    float maxVelocity = ToLocalVelocity(gfxPrefs::APZMaxVelocity());
-    newVelocity = std::min(newVelocity, maxVelocity);
-
-    if (gfxPrefs::APZCurveThreshold() > 0.0f && gfxPrefs::APZCurveThreshold() < gfxPrefs::APZMaxVelocity()) {
-      float curveThreshold = ToLocalVelocity(gfxPrefs::APZCurveThreshold());
-      if (newVelocity > curveThreshold) {
-        // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply the curve
-        float scale = maxVelocity - curveThreshold;
-        float funcInput = (newVelocity - curveThreshold) / scale;
-        float funcOutput =
-          gVelocityCurveFunction->GetValue(funcInput,
-            ComputedTimingFunction::BeforeFlag::Unset);
-        float curvedVelocity = (funcOutput * scale) + curveThreshold;
-        AXIS_LOG("%p|%s curving up velocity from %f to %f\n",
-          mAsyncPanZoomController, Name(), newVelocity, curvedVelocity);
-        newVelocity = curvedVelocity;
-      }
-    }
-
-    if (velocityIsNegative) {
-      newVelocity = -newVelocity;
-    }
-  }
-
-  return newVelocity;
-}
-
-void Axis::AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity) {
-  mVelocityQueue.AppendElement(std::make_pair(aTimestampMs, aVelocity));
-  if (mVelocityQueue.Length() > gfxPrefs::APZMaxVelocityQueueSize()) {
-    mVelocityQueue.RemoveElementAt(0);
+  if (Maybe<float> newVelocity = mVelocityTracker->AddPosition(aPos, aTimestampMs, mAxisLocked)) {
+    mVelocity = *newVelocity;
   }
 }
 
 void Axis::HandleDynamicToolbarMovement(uint32_t aStartTimestampMs,
                                         uint32_t aEndTimestampMs,
                                         ParentLayerCoord aDelta)
 {
-  // mVelocityQueue is controller-thread only
+  // mVelocityTracker is controller-thread only
   APZThreadUtils::AssertOnControllerThread();
 
-  float timeDelta = aEndTimestampMs - aStartTimestampMs;
-  MOZ_ASSERT(timeDelta != 0);
-  float speed = aDelta / timeDelta;
-  mVelocity = ApplyFlingCurveToVelocity(speed);
-  mVelocitySampleTimeMs = aEndTimestampMs;
-
-  AddVelocityToQueue(aEndTimestampMs, mVelocity);
+  mVelocity = mVelocityTracker->HandleDynamicToolbarMovement(aStartTimestampMs,
+                                                             aEndTimestampMs,
+                                                             aDelta);
 }
 
 void Axis::StartTouch(ParentLayerCoord aPos, uint32_t aTimestampMs) {
   mStartPos = aPos;
   mPos = aPos;
-  mVelocitySampleTimeMs = aTimestampMs;
-  mVelocitySamplePos = aPos;
+  mVelocityTracker->StartTracking(aPos, aTimestampMs);
   mAxisLocked = false;
 }
 
 bool Axis::AdjustDisplacement(ParentLayerCoord aDisplacement,
                               /* ParentLayerCoord */ float& aDisplacementOut,
                               /* ParentLayerCoord */ float& aOverscrollAmountOut,
                               bool aForceOverscroll /* = false */)
 {
@@ -312,41 +236,29 @@ ParentLayerCoord Axis::PanDistance(Paren
   return fabs(aPos - mStartPos);
 }
 
 void Axis::EndTouch(uint32_t aTimestampMs) {
   // mVelocityQueue is controller-thread only
   APZThreadUtils::AssertOnControllerThread();
 
   mAxisLocked = false;
-  mVelocity = 0;
-  int count = 0;
-  for (const auto& e : mVelocityQueue) {
-    uint32_t timeDelta = (aTimestampMs - e.first);
-    if (timeDelta < gfxPrefs::APZVelocityRelevanceTime()) {
-      count++;
-      mVelocity += e.second;
-    }
-  }
-  mVelocityQueue.Clear();
-  if (count > 1) {
-    mVelocity /= count;
-  }
+  mVelocity = mVelocityTracker->ComputeVelocity(aTimestampMs);
   AXIS_LOG("%p|%s ending touch, computed velocity %f\n",
     mAsyncPanZoomController, Name(), mVelocity);
 }
 
 void Axis::CancelGesture() {
   // mVelocityQueue is controller-thread only
   APZThreadUtils::AssertOnControllerThread();
 
   AXIS_LOG("%p|%s cancelling touch, clearing velocity queue\n",
     mAsyncPanZoomController, Name());
   mVelocity = 0.0f;
-  mVelocityQueue.Clear();
+  mVelocityTracker->Clear();
 }
 
 bool Axis::CanScroll() const {
   return GetPageLength() - GetCompositionLength() > COORDINATE_EPSILON;
 }
 
 bool Axis::CanScroll(ParentLayerCoord aDelta) const
 {
--- a/gfx/layers/apz/src/Axis.h
+++ b/gfx/layers/apz/src/Axis.h
@@ -30,16 +30,62 @@ const float EPSILON = 0.0001f;
  * for sufficiently large coordinate values).
  */
 bool FuzzyEqualsCoordinate(float aValue1, float aValue2);
 
 struct FrameMetrics;
 class AsyncPanZoomController;
 
 /**
+ * Interface for computing velocities along the axis based on
+ * position samples.
+ */
+class VelocityTracker {
+public:
+  virtual ~VelocityTracker() = default;
+
+  /**
+   * Start tracking velocity along this axis, starting with the given
+   * initial position and corresponding timestamp.
+   */
+  virtual void StartTracking(ParentLayerCoord aPos, uint32_t aTimestamp) = 0;
+  /**
+   * Record a new position along this axis, at the given timestamp.
+   * Returns the average velocity between the last sample and this one, or
+   * or Nothing() if a reasonable average cannot be computed.
+   * If |aIsAxisLocked| is true, no movement is happening along this axis,
+   * and this should be reflected both in the returned instantaneous velocity,
+   * and the internal state maintained for calling ComputeVelocity() later.
+   */
+  virtual Maybe<float> AddPosition(ParentLayerCoord aPos,
+                                   uint32_t aTimestampMs,
+                                   bool aIsAxisLocked) = 0;
+  /**
+   * Record movement of the dynamic toolbar along this axis by |aDelta|
+   * over the given time range. Movement of the dynamic toolbar means
+   * that physical movement by |aDelta| has occurred, but this will not
+   * be reflected in future positions passed to AddPosition().
+   * Returns the velocity of the dynamic toolbar movement.
+   */
+  virtual float HandleDynamicToolbarMovement(uint32_t aStartTimestampMs,
+                                             uint32_t aEndTimestampMs,
+                                             ParentLayerCoord aDelta) = 0;
+  /**
+   * Compute an estimate of the axis's current velocity, based on recent
+   * position samples. It's up to implementation how many samples to consider
+   * and how to perform the computation.
+   */
+  virtual float ComputeVelocity(uint32_t aTimestampMs) = 0;
+  /**
+   * Clear all state in the velocity tracker.
+   */
+  virtual void Clear() = 0;
+};
+
+/**
  * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
  * Note that everything here is specific to one axis; that is, the X axis knows
  * nothing about the Y axis and vice versa.
  */
 class Axis {
 public:
   explicit Axis(AsyncPanZoomController* aAsyncPanZoomController);
 
@@ -47,20 +93,16 @@ public:
    * Notify this Axis that a new touch has been received, including a timestamp
    * for when the touch was received. This triggers a recalculation of velocity.
    * This can also used for pan gesture events. For those events, |aPos| is
    * an invented position corresponding to the mouse position plus any
    * accumulated displacements over the course of the pan gesture.
    */
   void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, uint32_t aTimestampMs);
 
-protected:
-  float ApplyFlingCurveToVelocity(float aVelocity) const;
-  void AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity);
-
 public:
   void HandleDynamicToolbarMovement(uint32_t aStartTimestampMs,
                                     uint32_t aEndTimestampMs,
                                     ParentLayerCoord aDelta);
 
   /**
    * Notify this Axis that a touch has begun, i.e. the user has put their finger
    * on the screen but has not yet tried to pan.
@@ -247,64 +289,56 @@ public:
   virtual ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const = 0;
   virtual ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const = 0;
   virtual CSSToParentLayerScale GetScaleForAxis(const CSSToParentLayerScale2D& aScale) const = 0;
 
   virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0;
 
   virtual const char* Name() const = 0;
 
+  // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
+  float ToLocalVelocity(float aVelocityInchesPerMs) const;
+
 protected:
   // A position along the axis, used during input event processing to
   // track velocities (and for touch gestures, to track the length of
   // the gesture). For touch events, this represents the position of
   // the finger (or in the case of two-finger scrolling, the midpoint
   // of the two fingers). For pan gesture events, this represents an
   // invented position corresponding to the mouse position at the start
   // of the pan, plus deltas representing the displacement of the pan.
   ParentLayerCoord mPos;
 
-  // mVelocitySampleTimeMs and mVelocitySamplePos are the time and position
-  // used in the last velocity sampling. They get updated when a new sample is
-  // taken (which may not happen on every input event, if the time delta is too
-  // small).
-  uint32_t mVelocitySampleTimeMs;
-  ParentLayerCoord mVelocitySamplePos;
-
   ParentLayerCoord mStartPos;
   float mVelocity;      // Units: ParentLayerCoords per millisecond
   bool mAxisLocked;     // Whether movement on this axis is locked.
   AsyncPanZoomController* mAsyncPanZoomController;
 
   // The amount by which we are overscrolled; see GetOverscroll().
   ParentLayerCoord mOverscroll;
 
   // The mass-spring-damper model for overscroll physics.
   AxisPhysicsMSDModel mMSDModel;
 
-  // A queue of (timestamp, velocity) pairs; these are the historical
-  // velocities at the given timestamps. Timestamps are in milliseconds,
-  // velocities are in screen pixels per ms. This member can only be
-  // accessed on the controller/UI thread.
-  nsTArray<std::pair<uint32_t, float> > mVelocityQueue;
+  // Used to track velocity over a series of input events and compute
+  // a resulting velocity to use for e.g. starting a fling animation.
+  // This member can only be accessed on the controller/UI thread.
+  UniquePtr<VelocityTracker> mVelocityTracker;
 
   const FrameMetrics& GetFrameMetrics() const;
   const ScrollMetadata& GetScrollMetadata() const;
 
   virtual OverscrollBehavior GetOverscrollBehavior() const = 0;
 
   // Adjust a requested overscroll amount for resistance, yielding a smaller
   // actual overscroll amount.
   ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const;
 
   // Helper function for SampleOverscrollAnimation().
   void StepOverscrollAnimation(double aStepDurationMilliseconds);
-
-  // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
-  float ToLocalVelocity(float aVelocityInchesPerMs) const;
 };
 
 class AxisX : public Axis {
 public:
   explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController);
   virtual ParentLayerCoord GetPointOffset(const ParentLayerPoint& aPoint) const override;
   virtual ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
   virtual ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.cpp
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SimpleVelocityTracker.h"
+
+#include "gfxPrefs.h"
+#include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
+#include "mozilla/StaticPtr.h"              // for StaticAutoPtr
+
+#define SVT_LOG(...)
+// #define SVT_LOG(...) printf_stderr("SimpleVelocityTracker: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+// When we compute the velocity we do so by taking two input events and
+// dividing the distance delta over the time delta. In some cases the time
+// delta can be really small, which can make the velocity computation very
+// volatile. To avoid this we impose a minimum time delta below which we do
+// not recompute the velocity.
+const uint32_t MIN_VELOCITY_SAMPLE_TIME_MS = 5;
+
+extern StaticAutoPtr<ComputedTimingFunction> gVelocityCurveFunction;
+
+SimpleVelocityTracker::SimpleVelocityTracker(Axis* aAxis)
+  : mAxis(aAxis)
+  , mVelocitySampleTimeMs(0)
+  , mVelocitySamplePos(0)
+{
+}
+
+void
+SimpleVelocityTracker::StartTracking(ParentLayerCoord aPos, uint32_t aTimestampMs)
+{
+  Clear();
+  mVelocitySampleTimeMs = aTimestampMs;
+  mVelocitySamplePos = aPos;
+}
+
+Maybe<float>
+SimpleVelocityTracker::AddPosition(ParentLayerCoord aPos,
+                                   uint32_t aTimestampMs,
+                                   bool aIsAxisLocked)
+{
+  if (aTimestampMs <= mVelocitySampleTimeMs + MIN_VELOCITY_SAMPLE_TIME_MS) {
+    // See also the comment on MIN_VELOCITY_SAMPLE_TIME_MS.
+    // We still update mPos so that the positioning is correct (and we don't run
+    // into problems like bug 1042734) but the velocity will remain where it was.
+    // In particular we don't update either mVelocitySampleTimeMs or
+    // mVelocitySamplePos so that eventually when we do get an event with the
+    // required time delta we use the corresponding distance delta as well.
+    SVT_LOG("%p|%s skipping velocity computation for small time delta %dms\n",
+        mAxis->mAsyncPanZoomController, mAxis->Name(), (aTimestampMs - mVelocitySampleTimeMs));
+    return Nothing();
+  }
+
+  float newVelocity = aIsAxisLocked ? 0.0f : (float)(mVelocitySamplePos - aPos) / (float)(aTimestampMs - mVelocitySampleTimeMs);
+
+  newVelocity = ApplyFlingCurveToVelocity(newVelocity);
+
+  SVT_LOG("%p|%s updating velocity to %f with touch\n",
+    mAxis->mAsyncPanZoomController, mAxis->Name(), newVelocity);
+  mVelocitySampleTimeMs = aTimestampMs;
+  mVelocitySamplePos = aPos;
+
+  AddVelocityToQueue(aTimestampMs, newVelocity);
+
+  return Some(newVelocity);
+}
+
+float
+SimpleVelocityTracker::HandleDynamicToolbarMovement(uint32_t aStartTimestampMs,
+                                                    uint32_t aEndTimestampMs,
+                                                    ParentLayerCoord aDelta)
+{
+  float timeDelta = aEndTimestampMs - aStartTimestampMs;
+  MOZ_ASSERT(timeDelta != 0);
+  float velocity = aDelta / timeDelta;
+  velocity = ApplyFlingCurveToVelocity(velocity);
+  mVelocitySampleTimeMs = aEndTimestampMs;
+
+  AddVelocityToQueue(aEndTimestampMs, velocity);
+  return velocity;
+}
+
+float
+SimpleVelocityTracker::ComputeVelocity(uint32_t aTimestampMs)
+{
+  float velocity = 0;
+  int count = 0;
+  for (const auto& e : mVelocityQueue) {
+    uint32_t timeDelta = (aTimestampMs - e.first);
+    if (timeDelta < gfxPrefs::APZVelocityRelevanceTime()) {
+      count++;
+      velocity += e.second;
+    }
+  }
+  mVelocityQueue.Clear();
+  if (count > 1) {
+    velocity /= count;
+  }
+  return velocity;
+}
+
+void
+SimpleVelocityTracker::Clear()
+{
+  mVelocityQueue.Clear();
+}
+
+void
+SimpleVelocityTracker::AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity) {
+  mVelocityQueue.AppendElement(std::make_pair(aTimestampMs, aVelocity));
+  if (mVelocityQueue.Length() > gfxPrefs::APZMaxVelocityQueueSize()) {
+    mVelocityQueue.RemoveElementAt(0);
+  }
+}
+
+float
+SimpleVelocityTracker::ApplyFlingCurveToVelocity(float aVelocity) const {
+  float newVelocity = aVelocity;
+  if (gfxPrefs::APZMaxVelocity() > 0.0f) {
+    bool velocityIsNegative = (newVelocity < 0);
+    newVelocity = fabs(newVelocity);
+
+    float maxVelocity = mAxis->ToLocalVelocity(gfxPrefs::APZMaxVelocity());
+    newVelocity = std::min(newVelocity, maxVelocity);
+
+    if (gfxPrefs::APZCurveThreshold() > 0.0f && gfxPrefs::APZCurveThreshold() < gfxPrefs::APZMaxVelocity()) {
+      float curveThreshold = mAxis->ToLocalVelocity(gfxPrefs::APZCurveThreshold());
+      if (newVelocity > curveThreshold) {
+        // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply the curve
+        float scale = maxVelocity - curveThreshold;
+        float funcInput = (newVelocity - curveThreshold) / scale;
+        float funcOutput =
+          gVelocityCurveFunction->GetValue(funcInput,
+            ComputedTimingFunction::BeforeFlag::Unset);
+        float curvedVelocity = (funcOutput * scale) + curveThreshold;
+        SVT_LOG("%p|%s curving up velocity from %f to %f\n",
+          mAxis->mAsyncPanZoomController, mAxis->Name(), newVelocity, curvedVelocity);
+        newVelocity = curvedVelocity;
+      }
+    }
+
+    if (velocityIsNegative) {
+      newVelocity = -newVelocity;
+    }
+  }
+
+  return newVelocity;
+}
+
+}
+}
+
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/SimpleVelocityTracker.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_VelocityTracker_h
+#define mozilla_layers_VelocityTracker_h
+
+#include <utility>
+#include <cstdint>
+
+#include "Axis.h"
+#include "mozilla/Attributes.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace layers {
+
+class SimpleVelocityTracker : public VelocityTracker {
+public:
+  explicit SimpleVelocityTracker(Axis* aAxis);
+  void StartTracking(ParentLayerCoord aPos, uint32_t aTimestamp) override;
+  Maybe<float> AddPosition(ParentLayerCoord aPos,
+                           uint32_t aTimestampMs,
+                           bool aIsAxisLocked) override;
+  float HandleDynamicToolbarMovement(uint32_t aStartTimestampMs,
+                                     uint32_t aEndTimestampMs,
+                                     ParentLayerCoord aDelta) override;
+  float ComputeVelocity(uint32_t aTimestampMs) override;
+  void Clear() override;
+private:
+  void AddVelocityToQueue(uint32_t aTimestampMs, float aVelocity);
+  float ApplyFlingCurveToVelocity(float aVelocity) const;
+
+  // The Axis that uses this velocity tracker.
+  // This is a raw pointer because the Axis owns the velocity tracker
+  // by UniquePtr, so the velocity tracker cannot outlive the Axis.
+  Axis* MOZ_NON_OWNING_REF mAxis;
+
+  // A queue of (timestamp, velocity) pairs; these are the historical
+  // velocities at the given timestamps. Timestamps are in milliseconds,
+  // velocities are in screen pixels per ms. This member can only be
+  // accessed on the controller/UI thread.
+  nsTArray<std::pair<uint32_t, float>> mVelocityQueue;
+
+  // mVelocitySampleTimeMs and mVelocitySamplePos are the time and position
+  // used in the last velocity sampling. They get updated when a new sample is
+  // taken (which may not happen on every input event, if the time delta is too
+  // small).
+  uint32_t mVelocitySampleTimeMs;
+  ParentLayerCoord mVelocitySamplePos;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -322,16 +322,17 @@ UNIFIED_SOURCES += [
     'apz/src/InputBlockState.cpp',
     'apz/src/InputQueue.cpp',
     'apz/src/KeyboardMap.cpp',
     'apz/src/KeyboardScrollAction.cpp',
     'apz/src/KeyboardScrollAnimation.cpp',
     'apz/src/OverscrollHandoffState.cpp',
     'apz/src/PotentialCheckerboardDurationTracker.cpp',
     'apz/src/QueuedInput.cpp',
+    'apz/src/SimpleVelocityTracker.cpp',
     'apz/src/TouchCounter.cpp',
     'apz/src/WheelScrollAnimation.cpp',
     'apz/testutil/APZTestData.cpp',
     'apz/util/ActiveElementManager.cpp',
     'apz/util/APZCCallbackHelper.cpp',
     'apz/util/APZEventState.cpp',
     'apz/util/APZThreadUtils.cpp',
     'apz/util/CheckerboardReportService.cpp',