tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AndroidVelocityTracker.cpp (9991B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "AndroidVelocityTracker.h"
      8 
      9 #include "mozilla/StaticPrefs_apz.h"
     10 
     11 namespace mozilla {
     12 namespace layers {
     13 
     14 // This velocity tracker implementation was adapted from Chromium's
     15 // second-order unweighted least-squares velocity tracker strategy
     16 // (https://cs.chromium.org/chromium/src/ui/events/gesture_detection/velocity_tracker.cc?l=101&rcl=9ea9a086d4f54c702ec9a38e55fb3eb8bbc2401b).
     17 
     18 // Threshold between position updates for determining that a pointer has
     19 // stopped moving. Some input devices do not send move events in the
     20 // case where a pointer has stopped.  We need to detect this case so that we can
     21 // accurately predict the velocity after the pointer starts moving again.
     22 MOZ_RUNINIT static const TimeDuration kAssumePointerMoveStoppedTime =
     23    TimeDuration::FromMilliseconds(40);
     24 
     25 // The degree of the approximation.
     26 static const uint8_t kDegree = 2;
     27 
     28 // The degree of the polynomial used in SolveLeastSquares().
     29 // This should be the degree of the approximation plus one.
     30 static const uint8_t kPolyDegree = kDegree + 1;
     31 
     32 // Maximum size of position history.
     33 static const uint8_t kHistorySize = 20;
     34 
     35 AndroidVelocityTracker::AndroidVelocityTracker() {}
     36 
     37 void AndroidVelocityTracker::StartTracking(ParentLayerCoord aPos,
     38                                           TimeStamp aTimestamp) {
     39  Clear();
     40  mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
     41  mLastEventTime = aTimestamp;
     42 }
     43 
     44 Maybe<float> AndroidVelocityTracker::AddPosition(ParentLayerCoord aPos,
     45                                                 TimeStamp aTimestamp) {
     46  if ((aTimestamp - mLastEventTime) >= kAssumePointerMoveStoppedTime) {
     47    Clear();
     48  }
     49 
     50  if ((aTimestamp - mLastEventTime).ToMilliseconds() < 1.0) {
     51    // If we get a sample within a millisecond of the previous one,
     52    // just update its position. Two samples in the history with the
     53    // same timestamp can lead to things like infinite velocities.
     54    if (mHistory.Length() > 0) {
     55      mHistory[mHistory.Length() - 1].second = aPos;
     56    }
     57  } else {
     58    mHistory.AppendElement(std::make_pair(aTimestamp, aPos));
     59    if (mHistory.Length() > kHistorySize) {
     60      mHistory.RemoveElementAt(0);
     61    }
     62  }
     63 
     64  mLastEventTime = aTimestamp;
     65 
     66  if (mHistory.Length() < 2) {
     67    return Nothing();
     68  }
     69 
     70  auto start = mHistory[mHistory.Length() - 2];
     71  auto end = mHistory[mHistory.Length() - 1];
     72  auto velocity =
     73      (end.second - start.second) / (end.first - start.first).ToMilliseconds();
     74  // The velocity needs to be negated because the positions represent
     75  // touch positions, and the direction of scrolling is opposite to the
     76  // direction of the finger's movement.
     77  return Some(-velocity);
     78 }
     79 
     80 static float VectorDot(const float* a, const float* b, uint32_t m) {
     81  float r = 0;
     82  while (m--) {
     83    r += *(a++) * *(b++);
     84  }
     85  return r;
     86 }
     87 
     88 static float VectorNorm(const float* a, uint32_t m) {
     89  float r = 0;
     90  while (m--) {
     91    float t = *(a++);
     92    r += t * t;
     93  }
     94  return sqrtf(r);
     95 }
     96 
     97 /**
     98 * Solves a linear least squares problem to obtain a N degree polynomial that
     99 * fits the specified input data as nearly as possible.
    100 *
    101 * Returns true if a solution is found, false otherwise.
    102 *
    103 * The input consists of two vectors of data points X and Y with indices 0..m-1
    104 * along with a weight vector W of the same size.
    105 *
    106 * The output is a vector B with indices 0..n that describes a polynomial
    107 * that fits the data, such the sum of W[i] * W[i] * abs(Y[i] - (B[0] + B[1]
    108 * X[i] * + B[2] X[i]^2 ... B[n] X[i]^n)) for all i between 0 and m-1 is
    109 * minimized.
    110 *
    111 * Accordingly, the weight vector W should be initialized by the caller with the
    112 * reciprocal square root of the variance of the error in each input data point.
    113 * In other words, an ideal choice for W would be W[i] = 1 / var(Y[i]) = 1 /
    114 * stddev(Y[i]).
    115 * The weights express the relative importance of each data point.  If the
    116 * weights are* all 1, then the data points are considered to be of equal
    117 * importance when fitting the polynomial.  It is a good idea to choose weights
    118 * that diminish the importance of data points that may have higher than usual
    119 * error margins.
    120 *
    121 * Errors among data points are assumed to be independent.  W is represented
    122 * here as a vector although in the literature it is typically taken to be a
    123 * diagonal matrix.
    124 *
    125 * That is to say, the function that generated the input data can be
    126 * approximated by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n.
    127 *
    128 * The coefficient of determination (R^2) is also returned to describe the
    129 * goodness of fit of the model for the given data.  It is a value between 0
    130 * and 1, where 1 indicates perfect correspondence.
    131 *
    132 * This function first expands the X vector to a m by n matrix A such that
    133 * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n, then
    134 * multiplies it by w[i].
    135 *
    136 * Then it calculates the QR decomposition of A yielding an m by m orthonormal
    137 * matrix Q and an m by n upper triangular matrix R.  Because R is upper
    138 * triangular (lower part is all zeroes), we can simplify the decomposition into
    139 * an m by n matrix Q1 and a n by n matrix R1 such that A = Q1 R1.
    140 *
    141 * Finally we solve the system of linear equations given by
    142 * R1 B = (Qtranspose W Y) to find B.
    143 *
    144 * For efficiency, we lay out A and Q column-wise in memory because we
    145 * frequently operate on the column vectors.  Conversely, we lay out R row-wise.
    146 *
    147 * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares
    148 * http://en.wikipedia.org/wiki/Gram-Schmidt
    149 */
    150 static bool SolveLeastSquares(const float* x, const float* y, const float* w,
    151                              uint32_t m, uint32_t n, float* out_b) {
    152  // MSVC does not support variable-length arrays (used by the original Android
    153  // implementation of this function).
    154 #if defined(COMPILER_MSVC)
    155  const uint32_t M_ARRAY_LENGTH = VelocityTracker::kHistorySize;
    156  const uint32_t N_ARRAY_LENGTH = VelocityTracker::kPolyDegree;
    157  DCHECK_LE(m, M_ARRAY_LENGTH);
    158  DCHECK_LE(n, N_ARRAY_LENGTH);
    159 #else
    160  const uint32_t M_ARRAY_LENGTH = m;
    161  const uint32_t N_ARRAY_LENGTH = n;
    162 #endif
    163 
    164  // Expand the X vector to a matrix A, pre-multiplied by the weights.
    165  float a[N_ARRAY_LENGTH][M_ARRAY_LENGTH];  // column-major order
    166  for (uint32_t h = 0; h < m; h++) {
    167    a[0][h] = w[h];
    168    for (uint32_t i = 1; i < n; i++) {
    169      a[i][h] = a[i - 1][h] * x[h];
    170    }
    171  }
    172 
    173  // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
    174 
    175  // Orthonormal basis, column-major order.
    176  float q[N_ARRAY_LENGTH][M_ARRAY_LENGTH];
    177  // Upper triangular matrix, row-major order.
    178  float r[N_ARRAY_LENGTH][N_ARRAY_LENGTH];
    179  for (uint32_t j = 0; j < n; j++) {
    180    for (uint32_t h = 0; h < m; h++) {
    181      q[j][h] = a[j][h];
    182    }
    183    for (uint32_t i = 0; i < j; i++) {
    184      float dot = VectorDot(&q[j][0], &q[i][0], m);
    185      for (uint32_t h = 0; h < m; h++) {
    186        q[j][h] -= dot * q[i][h];
    187      }
    188    }
    189 
    190    float norm = VectorNorm(&q[j][0], m);
    191    if (norm < 0.000001f) {
    192      // vectors are linearly dependent or zero so no solution
    193      return false;
    194    }
    195 
    196    float invNorm = 1.0f / norm;
    197    for (uint32_t h = 0; h < m; h++) {
    198      q[j][h] *= invNorm;
    199    }
    200    for (uint32_t i = 0; i < n; i++) {
    201      r[j][i] = i < j ? 0 : VectorDot(&q[j][0], &a[i][0], m);
    202    }
    203  }
    204 
    205  // Solve R B = Qt W Y to find B.  This is easy because R is upper triangular.
    206  // We just work from bottom-right to top-left calculating B's coefficients.
    207  float wy[M_ARRAY_LENGTH];
    208  for (uint32_t h = 0; h < m; h++) {
    209    wy[h] = y[h] * w[h];
    210  }
    211  for (uint32_t i = n; i-- != 0;) {
    212    out_b[i] = VectorDot(&q[i][0], wy, m);
    213    for (uint32_t j = n - 1; j > i; j--) {
    214      out_b[i] -= r[i][j] * out_b[j];
    215    }
    216    out_b[i] /= r[i][i];
    217  }
    218 
    219  return true;
    220 }
    221 
    222 Maybe<float> AndroidVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) {
    223  if (mHistory.IsEmpty()) {
    224    return Nothing{};
    225  }
    226 
    227  // Polynomial coefficients describing motion along the axis.
    228  float xcoeff[kPolyDegree + 1];
    229  for (size_t i = 0; i <= kPolyDegree; i++) {
    230    xcoeff[i] = 0;
    231  }
    232 
    233  // Iterate over movement samples in reverse time order and collect samples.
    234  float pos[kHistorySize];
    235  float w[kHistorySize];
    236  float time[kHistorySize];
    237  uint32_t m = 0;
    238  int index = mHistory.Length() - 1;
    239  const TimeDuration horizon = TimeDuration::FromMilliseconds(
    240      StaticPrefs::apz_velocity_relevance_time_ms());
    241  const auto& newest_movement = mHistory[index];
    242 
    243  do {
    244    const auto& movement = mHistory[index];
    245    TimeDuration age = newest_movement.first - movement.first;
    246    if (age > horizon) break;
    247 
    248    ParentLayerCoord position = movement.second;
    249    pos[m] = position;
    250    w[m] = 1.0f;
    251    time[m] =
    252        -static_cast<float>(age.ToMilliseconds()) / 1000.0f;  // in seconds
    253    index--;
    254    m++;
    255  } while (index >= 0);
    256 
    257  if (m == 0) {
    258    return Nothing{};  // no data
    259  }
    260 
    261  // Calculate a least squares polynomial fit.
    262 
    263  // Polynomial degree (number of coefficients), or zero if no information is
    264  // available.
    265  uint32_t degree = kDegree;
    266  if (degree > m - 1) {
    267    degree = m - 1;
    268  }
    269 
    270  if (degree >= 1) {  // otherwise, no velocity data available
    271    uint32_t n = degree + 1;
    272    if (SolveLeastSquares(time, pos, w, m, n, xcoeff)) {
    273      float velocity = xcoeff[1];
    274 
    275      // The velocity needs to be negated because the positions represent
    276      // touch positions, and the direction of scrolling is opposite to the
    277      // direction of the finger's movement.
    278      return Some(-velocity / 1000.0f);  // convert to pixels per millisecond
    279    }
    280  }
    281 
    282  return Nothing{};
    283 }
    284 
    285 void AndroidVelocityTracker::Clear() { mHistory.Clear(); }
    286 
    287 }  // namespace layers
    288 }  // namespace mozilla