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