summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/src/AndroidVelocityTracker.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /gfx/layers/apz/src/AndroidVelocityTracker.cpp
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/layers/apz/src/AndroidVelocityTracker.cpp')
-rw-r--r--gfx/layers/apz/src/AndroidVelocityTracker.cpp288
1 files changed, 288 insertions, 0 deletions
diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.cpp b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
new file mode 100644
index 0000000000..a355811a00
--- /dev/null
+++ b/gfx/layers/apz/src/AndroidVelocityTracker.cpp
@@ -0,0 +1,288 @@
+/* -*- 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 "AndroidVelocityTracker.h"
+
+#include "mozilla/StaticPrefs_apz.h"
+
+namespace mozilla {
+namespace layers {
+
+// This velocity tracker implementation was adapted from Chromium's
+// second-order unweighted least-squares velocity tracker strategy
+// (https://cs.chromium.org/chromium/src/ui/events/gesture_detection/velocity_tracker.cc?l=101&rcl=9ea9a086d4f54c702ec9a38e55fb3eb8bbc2401b).
+
+// Threshold between position updates for determining that a pointer has
+// stopped moving. Some input devices do not send move events in the
+// case where a pointer has stopped. We need to detect this case so that we can
+// accurately predict the velocity after the pointer starts moving again.
+static const TimeDuration kAssumePointerMoveStoppedTime =
+ TimeDuration::FromMilliseconds(40);
+
+// The degree of the approximation.
+static const uint8_t kDegree = 2;
+
+// The degree of the polynomial used in SolveLeastSquares().
+// This should be the degree of the approximation plus one.
+static const uint8_t kPolyDegree = kDegree + 1;
+
+// Maximum size of position history.
+static const uint8_t kHistorySize = 20;
+
+AndroidVelocityTracker::AndroidVelocityTracker() {}
+
+void AndroidVelocityTracker::StartTracking(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ Clear();
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ mLastEventTime = aTimestamp;
+}
+
+Maybe<float> AndroidVelocityTracker::AddPosition(ParentLayerCoord aPos,
+ TimeStamp aTimestamp) {
+ if ((aTimestamp - mLastEventTime) >= kAssumePointerMoveStoppedTime) {
+ Clear();
+ }
+
+ if ((aTimestamp - mLastEventTime).ToMilliseconds() < 1.0) {
+ // If we get a sample within a millisecond of the previous one,
+ // just update its position. Two samples in the history with the
+ // same timestamp can lead to things like infinite velocities.
+ if (mHistory.Length() > 0) {
+ mHistory[mHistory.Length() - 1].second = aPos;
+ }
+ } else {
+ mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
+ if (mHistory.Length() > kHistorySize) {
+ mHistory.RemoveElementAt(0);
+ }
+ }
+
+ mLastEventTime = aTimestamp;
+
+ if (mHistory.Length() < 2) {
+ return Nothing();
+ }
+
+ auto start = mHistory[mHistory.Length() - 2];
+ auto end = mHistory[mHistory.Length() - 1];
+ auto velocity =
+ (end.second - start.second) / (end.first - start.first).ToMilliseconds();
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity);
+}
+
+static float VectorDot(const float* a, const float* b, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ r += *(a++) * *(b++);
+ }
+ return r;
+}
+
+static float VectorNorm(const float* a, uint32_t m) {
+ float r = 0;
+ while (m--) {
+ float t = *(a++);
+ r += t * t;
+ }
+ return sqrtf(r);
+}
+
+/**
+ * Solves a linear least squares problem to obtain a N degree polynomial that
+ * fits the specified input data as nearly as possible.
+ *
+ * Returns true if a solution is found, false otherwise.
+ *
+ * The input consists of two vectors of data points X and Y with indices 0..m-1
+ * along with a weight vector W of the same size.
+ *
+ * The output is a vector B with indices 0..n that describes a polynomial
+ * that fits the data, such the sum of W[i] * W[i] * abs(Y[i] - (B[0] + B[1]
+ * X[i] * + B[2] X[i]^2 ... B[n] X[i]^n)) for all i between 0 and m-1 is
+ * minimized.
+ *
+ * Accordingly, the weight vector W should be initialized by the caller with the
+ * reciprocal square root of the variance of the error in each input data point.
+ * In other words, an ideal choice for W would be W[i] = 1 / var(Y[i]) = 1 /
+ * stddev(Y[i]).
+ * The weights express the relative importance of each data point. If the
+ * weights are* all 1, then the data points are considered to be of equal
+ * importance when fitting the polynomial. It is a good idea to choose weights
+ * that diminish the importance of data points that may have higher than usual
+ * error margins.
+ *
+ * Errors among data points are assumed to be independent. W is represented
+ * here as a vector although in the literature it is typically taken to be a
+ * diagonal matrix.
+ *
+ * That is to say, the function that generated the input data can be
+ * approximated by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n.
+ *
+ * The coefficient of determination (R^2) is also returned to describe the
+ * goodness of fit of the model for the given data. It is a value between 0
+ * and 1, where 1 indicates perfect correspondence.
+ *
+ * This function first expands the X vector to a m by n matrix A such that
+ * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n, then
+ * multiplies it by w[i].
+ *
+ * Then it calculates the QR decomposition of A yielding an m by m orthonormal
+ * matrix Q and an m by n upper triangular matrix R. Because R is upper
+ * triangular (lower part is all zeroes), we can simplify the decomposition into
+ * an m by n matrix Q1 and a n by n matrix R1 such that A = Q1 R1.
+ *
+ * Finally we solve the system of linear equations given by
+ * R1 B = (Qtranspose W Y) to find B.
+ *
+ * For efficiency, we lay out A and Q column-wise in memory because we
+ * frequently operate on the column vectors. Conversely, we lay out R row-wise.
+ *
+ * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares
+ * http://en.wikipedia.org/wiki/Gram-Schmidt
+ */
+static bool SolveLeastSquares(const float* x, const float* y, const float* w,
+ uint32_t m, uint32_t n, float* out_b) {
+ // MSVC does not support variable-length arrays (used by the original Android
+ // implementation of this function).
+#if defined(COMPILER_MSVC)
+ const uint32_t M_ARRAY_LENGTH = VelocityTracker::kHistorySize;
+ const uint32_t N_ARRAY_LENGTH = VelocityTracker::kPolyDegree;
+ DCHECK_LE(m, M_ARRAY_LENGTH);
+ DCHECK_LE(n, N_ARRAY_LENGTH);
+#else
+ const uint32_t M_ARRAY_LENGTH = m;
+ const uint32_t N_ARRAY_LENGTH = n;
+#endif
+
+ // Expand the X vector to a matrix A, pre-multiplied by the weights.
+ float a[N_ARRAY_LENGTH][M_ARRAY_LENGTH]; // column-major order
+ for (uint32_t h = 0; h < m; h++) {
+ a[0][h] = w[h];
+ for (uint32_t i = 1; i < n; i++) {
+ a[i][h] = a[i - 1][h] * x[h];
+ }
+ }
+
+ // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
+
+ // Orthonormal basis, column-major order.
+ float q[N_ARRAY_LENGTH][M_ARRAY_LENGTH];
+ // Upper triangular matrix, row-major order.
+ float r[N_ARRAY_LENGTH][N_ARRAY_LENGTH];
+ for (uint32_t j = 0; j < n; j++) {
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] = a[j][h];
+ }
+ for (uint32_t i = 0; i < j; i++) {
+ float dot = VectorDot(&q[j][0], &q[i][0], m);
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] -= dot * q[i][h];
+ }
+ }
+
+ float norm = VectorNorm(&q[j][0], m);
+ if (norm < 0.000001f) {
+ // vectors are linearly dependent or zero so no solution
+ return false;
+ }
+
+ float invNorm = 1.0f / norm;
+ for (uint32_t h = 0; h < m; h++) {
+ q[j][h] *= invNorm;
+ }
+ for (uint32_t i = 0; i < n; i++) {
+ r[j][i] = i < j ? 0 : VectorDot(&q[j][0], &a[i][0], m);
+ }
+ }
+
+ // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
+ // We just work from bottom-right to top-left calculating B's coefficients.
+ float wy[M_ARRAY_LENGTH];
+ for (uint32_t h = 0; h < m; h++) {
+ wy[h] = y[h] * w[h];
+ }
+ for (uint32_t i = n; i-- != 0;) {
+ out_b[i] = VectorDot(&q[i][0], wy, m);
+ for (uint32_t j = n - 1; j > i; j--) {
+ out_b[i] -= r[i][j] * out_b[j];
+ }
+ out_b[i] /= r[i][i];
+ }
+
+ return true;
+}
+
+Maybe<float> AndroidVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
+ if (mHistory.IsEmpty()) {
+ return Nothing{};
+ }
+
+ // Polynomial coefficients describing motion along the axis.
+ float xcoeff[kPolyDegree + 1];
+ for (size_t i = 0; i <= kPolyDegree; i++) {
+ xcoeff[i] = 0;
+ }
+
+ // Iterate over movement samples in reverse time order and collect samples.
+ float pos[kHistorySize];
+ float w[kHistorySize];
+ float time[kHistorySize];
+ uint32_t m = 0;
+ int index = mHistory.Length() - 1;
+ const TimeDuration horizon = TimeDuration::FromMilliseconds(
+ StaticPrefs::apz_velocity_relevance_time_ms());
+ const auto& newest_movement = mHistory[index];
+
+ do {
+ const auto& movement = mHistory[index];
+ TimeDuration age = newest_movement.first - movement.first;
+ if (age > horizon) break;
+
+ ParentLayerCoord position = movement.second;
+ pos[m] = position;
+ w[m] = 1.0f;
+ time[m] =
+ -static_cast<float>(age.ToMilliseconds()) / 1000.0f; // in seconds
+ index--;
+ m++;
+ } while (index >= 0);
+
+ if (m == 0) {
+ return Nothing{}; // no data
+ }
+
+ // Calculate a least squares polynomial fit.
+
+ // Polynomial degree (number of coefficients), or zero if no information is
+ // available.
+ uint32_t degree = kDegree;
+ if (degree > m - 1) {
+ degree = m - 1;
+ }
+
+ if (degree >= 1) { // otherwise, no velocity data available
+ uint32_t n = degree + 1;
+ if (SolveLeastSquares(time, pos, w, m, n, xcoeff)) {
+ float velocity = xcoeff[1];
+
+ // The velocity needs to be negated because the positions represent
+ // touch positions, and the direction of scrolling is opposite to the
+ // direction of the finger's movement.
+ return Some(-velocity / 1000.0f); // convert to pixels per millisecond
+ }
+ }
+
+ return Nothing{};
+}
+
+void AndroidVelocityTracker::Clear() { mHistory.Clear(); }
+
+} // namespace layers
+} // namespace mozilla