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 "TouchResampler.h"
8 
9 #include "nsAlgorithm.h"
10 
11 /**
12  * TouchResampler implementation
13  */
14 
15 namespace mozilla {
16 namespace widget {
17 
18 // The values below have been tested and found to be acceptable on a device
19 // with a display refresh rate of 60Hz and touch sampling rate of 100Hz.
20 // While their "ideal" values are dependent on the exact rates of each device,
21 // the values we've picked below should be somewhat robust across a variation of
22 // different rates. They mostly aim to avoid making predictions that are too far
23 // away (in terms of distance) from the finger, and to detect pauses in the
24 // finger motion without too much delay.
25 
26 // Maximum time between two consecutive data points to consider resampling
27 // between them.
28 // Values between 1x and 5x of the touch sampling interval are reasonable.
29 static const double kTouchResampleWindowSize = 40.0;
30 
31 // These next two values constrain the sampling timestamp.
32 // Our caller will usually adjust frame timestamps to be slightly in the past,
33 // for example by 5ms. This means that, during normal operation, we will
34 // maximally need to predict by [touch sampling rate] minus 5ms.
35 // So we would like kTouchResampleMaxPredictMs to satisfy the following:
36 // kTouchResampleMaxPredictMs + [frame time adjust] > [touch sampling rate]
37 static const double kTouchResampleMaxPredictMs = 8.0;
38 // This one is a protection against very outdated frame timestamps.
39 // Values larger than the touch sampling interval and less than 3x of the vsync
40 // interval are reasonable.
41 static const double kTouchResampleMaxBacksampleMs = 20.0;
42 
43 // The maximum age of the most recent data point to consider resampling.
44 // Should be between 1x and 3x of the touch sampling interval.
45 static const double kTouchResampleOldTouchThresholdMs = 17.0;
46 
ProcessEvent(MultiTouchInput && aInput)47 uint64_t TouchResampler::ProcessEvent(MultiTouchInput&& aInput) {
48   mCurrentTouches.UpdateFromEvent(aInput);
49 
50   uint64_t eventId = mNextEventId;
51   mNextEventId++;
52 
53   if (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) {
54     // Touch move events are deferred until NotifyFrame.
55     mDeferredTouchMoveEvents.push({std::move(aInput), eventId});
56   } else {
57     // Non-move events are transferred to the outgoing queue unmodified.
58     // If there are pending touch move events, flush those out first, so that
59     // events are emitted in the right order.
60     FlushDeferredTouchMoveEventsUnresampled();
61     if (mInResampledState) {
62       // Return to a non-resampled state before emitting a non-move event.
63       ReturnToNonResampledState();
64     }
65     EmitEvent(std::move(aInput), eventId);
66   }
67 
68   return eventId;
69 }
70 
NotifyFrame(const TimeStamp & aTimeStamp)71 void TouchResampler::NotifyFrame(const TimeStamp& aTimeStamp) {
72   TimeStamp lastTouchTime = mCurrentTouches.LatestDataPointTime();
73   if (mDeferredTouchMoveEvents.empty() ||
74       (lastTouchTime &&
75        lastTouchTime < aTimeStamp - TimeDuration::FromMilliseconds(
76                                         kTouchResampleOldTouchThresholdMs))) {
77     // We haven't received a touch move event in a while, so the fingers must
78     // have stopped moving. Flush any old touch move events.
79     FlushDeferredTouchMoveEventsUnresampled();
80 
81     if (mInResampledState) {
82       // Make sure we pause at the resting position that we actually observed,
83       // and not at a resampled position.
84       ReturnToNonResampledState();
85     }
86 
87     // Clear touch location history so that we don't resample across a pause.
88     mCurrentTouches.ClearDataPoints();
89     return;
90   }
91 
92   MOZ_RELEASE_ASSERT(lastTouchTime);
93   TimeStamp lowerBound = lastTouchTime - TimeDuration::FromMilliseconds(
94                                              kTouchResampleMaxBacksampleMs);
95   TimeStamp upperBound = lastTouchTime + TimeDuration::FromMilliseconds(
96                                              kTouchResampleMaxPredictMs);
97   TimeStamp sampleTime = clamped(aTimeStamp, lowerBound, upperBound);
98 
99   if (mLastEmittedEventTime && sampleTime < mLastEmittedEventTime) {
100     // Keep emitted timestamps in order.
101     sampleTime = mLastEmittedEventTime;
102   }
103 
104   // We have at least one pending touch move event. Pick one of the events from
105   // mDeferredTouchMoveEvents as the base event for the resampling adjustment.
106   // We want to produce an event stream whose timestamps are in the right order.
107   // As the base event, use the first event that's at or after sampleTime,
108   // unless there is no such event, in that case use the last one we have. We
109   // will set the timestamp on the resampled event to sampleTime later.
110   // Flush out any older events so that everything remains in the right order.
111   MultiTouchInput input;
112   uint64_t eventId;
113   while (true) {
114     MOZ_RELEASE_ASSERT(!mDeferredTouchMoveEvents.empty());
115 
116     std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front());
117     mDeferredTouchMoveEvents.pop();
118     if (mDeferredTouchMoveEvents.empty() || input.mTimeStamp >= sampleTime) {
119       break;
120     }
121 
122     // Flush this event to the outgoing queue without resampling. What ends up
123     // on the screen will still be smooth because we will proceed to emit a
124     // resampled event before the paint for this frame starts.
125     PrependLeftoverHistoricalData(&input);
126     MOZ_RELEASE_ASSERT(input.mTimeStamp < sampleTime);
127     EmitEvent(std::move(input), eventId);
128   }
129 
130   mOriginalOfResampledTouchMove = Nothing();
131 
132   // Compute the resampled touch positions.
133   nsTArray<ScreenIntPoint> resampledPositions;
134   bool anyPositionDifferentFromOriginal = false;
135   for (const auto& touch : input.mTouches) {
136     ScreenIntPoint resampledPosition =
137         mCurrentTouches.ResampleTouchPositionAtTime(
138             touch.mIdentifier, touch.mScreenPoint, sampleTime);
139     if (resampledPosition != touch.mScreenPoint) {
140       anyPositionDifferentFromOriginal = true;
141     }
142     resampledPositions.AppendElement(resampledPosition);
143   }
144 
145   if (anyPositionDifferentFromOriginal) {
146     // Store a copy of the original event, so that we can return to an
147     // non-resampled position later, if necessary.
148     mOriginalOfResampledTouchMove = Some(input);
149 
150     // Add the original observed position to the historical data, as well as any
151     // leftover historical positions from the previous touch move event, and
152     // store the resampled values in the "final" position of the event.
153     PrependLeftoverHistoricalData(&input);
154     for (size_t i = 0; i < input.mTouches.Length(); i++) {
155       auto& touch = input.mTouches[i];
156       touch.mHistoricalData.AppendElement(SingleTouchData::HistoricalTouchData{
157           input.mTimeStamp,
158           touch.mScreenPoint,
159           touch.mLocalScreenPoint,
160           touch.mRadius,
161           touch.mRotationAngle,
162           touch.mForce,
163       });
164 
165       // Remove any historical touch data that's in the future, compared to
166       // sampleTime. This data will be included by upcoming touch move
167       // events. This only happens if the frame timestamp can be older than the
168       // event timestamp, i.e. if interpolation occurs (rather than
169       // extrapolation).
170       auto futureDataStart = std::find_if(
171           touch.mHistoricalData.begin(), touch.mHistoricalData.end(),
172           [sampleTime](
173               const SingleTouchData::HistoricalTouchData& aHistoricalData) {
174             return aHistoricalData.mTimeStamp > sampleTime;
175           });
176       if (futureDataStart != touch.mHistoricalData.end()) {
177         nsTArray<SingleTouchData::HistoricalTouchData> futureData(
178             Span<SingleTouchData::HistoricalTouchData>(touch.mHistoricalData)
179                 .From(futureDataStart.GetIndex()));
180         touch.mHistoricalData.TruncateLength(futureDataStart.GetIndex());
181         mRemainingTouchData.insert({touch.mIdentifier, std::move(futureData)});
182       }
183 
184       touch.mScreenPoint = resampledPositions[i];
185     }
186     input.mTimeStamp = sampleTime;
187   }
188 
189   EmitEvent(std::move(input), eventId);
190   mInResampledState = anyPositionDifferentFromOriginal;
191 }
192 
PrependLeftoverHistoricalData(MultiTouchInput * aInput)193 void TouchResampler::PrependLeftoverHistoricalData(MultiTouchInput* aInput) {
194   for (auto& touch : aInput->mTouches) {
195     auto leftoverData = mRemainingTouchData.find(touch.mIdentifier);
196     if (leftoverData != mRemainingTouchData.end()) {
197       nsTArray<SingleTouchData::HistoricalTouchData> data =
198           std::move(leftoverData->second);
199       mRemainingTouchData.erase(leftoverData);
200       touch.mHistoricalData.InsertElementsAt(0, data);
201     }
202 
203     if (TimeStamp cutoffTime = mLastEmittedEventTime) {
204       // If we received historical touch data that was further in the past than
205       // the last resampled event, discard that data so that the touch data
206       // points are emitted in order.
207       touch.mHistoricalData.RemoveElementsBy(
208           [cutoffTime](const SingleTouchData::HistoricalTouchData& aTouchData) {
209             return aTouchData.mTimeStamp < cutoffTime;
210           });
211     }
212   }
213   mRemainingTouchData.clear();
214 }
215 
FlushDeferredTouchMoveEventsUnresampled()216 void TouchResampler::FlushDeferredTouchMoveEventsUnresampled() {
217   while (!mDeferredTouchMoveEvents.empty()) {
218     MultiTouchInput input;
219     uint64_t eventId;
220     std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front());
221     mDeferredTouchMoveEvents.pop();
222     PrependLeftoverHistoricalData(&input);
223     EmitEvent(std::move(input), eventId);
224     mInResampledState = false;
225     mOriginalOfResampledTouchMove = Nothing();
226   }
227 }
228 
ReturnToNonResampledState()229 void TouchResampler::ReturnToNonResampledState() {
230   MOZ_RELEASE_ASSERT(mInResampledState);
231   MOZ_RELEASE_ASSERT(mDeferredTouchMoveEvents.empty(),
232                      "Don't call this if there is a deferred touch move event. "
233                      "We can return to the non-resampled state by sending that "
234                      "event, rather than a copy of a previous event.");
235 
236   // The last outgoing event was a resampled touch move event.
237   // Return to the non-resampled state, by sending a touch move event to
238   // "overwrite" any resampled positions with the original observed positions.
239   MultiTouchInput input = std::move(*mOriginalOfResampledTouchMove);
240   mOriginalOfResampledTouchMove = Nothing();
241 
242   // For the event's timestamp, we want to backdate the correction as far as we
243   // can, while still preserving timestamp ordering. But we also don't want to
244   // backdate it to be older than it was originally.
245   if (mLastEmittedEventTime > input.mTimeStamp) {
246     input.mTimeStamp = mLastEmittedEventTime;
247   }
248 
249   // Assemble the correct historical touch data for this event.
250   // We don't want to include data points that we've already sent out with the
251   // resampled event. And from the leftover data points, we only want those that
252   // don't duplicate the final time + position of this event.
253   for (auto& touch : input.mTouches) {
254     touch.mHistoricalData.Clear();
255   }
256   PrependLeftoverHistoricalData(&input);
257   for (auto& touch : input.mTouches) {
258     touch.mHistoricalData.RemoveElementsBy([&](const auto& histData) {
259       return histData.mTimeStamp >= input.mTimeStamp;
260     });
261   }
262 
263   EmitExtraEvent(std::move(input));
264   mInResampledState = false;
265 }
266 
Update(const SingleTouchData & aTouch,const TimeStamp & aEventTime)267 void TouchResampler::TouchInfo::Update(const SingleTouchData& aTouch,
268                                        const TimeStamp& aEventTime) {
269   for (const auto& historicalData : aTouch.mHistoricalData) {
270     mBaseDataPoint = mLatestDataPoint;
271     mLatestDataPoint =
272         Some(DataPoint{historicalData.mTimeStamp, historicalData.mScreenPoint});
273   }
274   mBaseDataPoint = mLatestDataPoint;
275   mLatestDataPoint = Some(DataPoint{aEventTime, aTouch.mScreenPoint});
276 }
277 
ResampleAtTime(const ScreenIntPoint & aLastObservedPosition,const TimeStamp & aTimeStamp)278 ScreenIntPoint TouchResampler::TouchInfo::ResampleAtTime(
279     const ScreenIntPoint& aLastObservedPosition, const TimeStamp& aTimeStamp) {
280   TimeStamp cutoff =
281       aTimeStamp - TimeDuration::FromMilliseconds(kTouchResampleWindowSize);
282   if (!mBaseDataPoint || !mLatestDataPoint ||
283       !(mBaseDataPoint->mTimeStamp < mLatestDataPoint->mTimeStamp) ||
284       mBaseDataPoint->mTimeStamp < cutoff) {
285     return aLastObservedPosition;
286   }
287 
288   // For the actual resampling, connect the last two data points with a line and
289   // sample along that line.
290   TimeStamp t1 = mBaseDataPoint->mTimeStamp;
291   TimeStamp t2 = mLatestDataPoint->mTimeStamp;
292   double t = (aTimeStamp - t1) / (t2 - t1);
293 
294   double x1 = mBaseDataPoint->mPosition.x;
295   double x2 = mLatestDataPoint->mPosition.x;
296   double y1 = mBaseDataPoint->mPosition.y;
297   double y2 = mLatestDataPoint->mPosition.y;
298 
299   int32_t resampledX = round(x1 + t * (x2 - x1));
300   int32_t resampledY = round(y1 + t * (y2 - y1));
301   return ScreenIntPoint(resampledX, resampledY);
302 }
303 
UpdateFromEvent(const MultiTouchInput & aInput)304 void TouchResampler::CurrentTouches::UpdateFromEvent(
305     const MultiTouchInput& aInput) {
306   switch (aInput.mType) {
307     case MultiTouchInput::MULTITOUCH_START: {
308       // A new touch has been added; make sure mTouches reflects the current
309       // touches in the event.
310       nsTArray<TouchInfo> newTouches;
311       for (const auto& touch : aInput.mTouches) {
312         const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
313         if (touchInfo != mTouches.end()) {
314           // This is one of the existing touches.
315           newTouches.AppendElement(std::move(*touchInfo));
316           mTouches.RemoveElementAt(touchInfo);
317         } else {
318           // This is the new touch.
319           newTouches.AppendElement(TouchInfo{
320               touch.mIdentifier, Nothing(),
321               Some(DataPoint{aInput.mTimeStamp, touch.mScreenPoint})});
322         }
323       }
324       MOZ_ASSERT(mTouches.IsEmpty(), "Missing touch end before touch start?");
325       mTouches = std::move(newTouches);
326       break;
327     }
328 
329     case MultiTouchInput::MULTITOUCH_MOVE: {
330       // The touches have moved.
331       // Add position information to the history data points.
332       for (const auto& touch : aInput.mTouches) {
333         const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
334         MOZ_ASSERT(touchInfo != mTouches.end());
335         if (touchInfo != mTouches.end()) {
336           touchInfo->Update(touch, aInput.mTimeStamp);
337         }
338       }
339       mLatestDataPointTime = aInput.mTimeStamp;
340       break;
341     }
342 
343     case MultiTouchInput::MULTITOUCH_END: {
344       // A touch has been removed.
345       MOZ_RELEASE_ASSERT(aInput.mTouches.Length() == 1);
346       const auto touchInfo = TouchByIdentifier(aInput.mTouches[0].mIdentifier);
347       MOZ_ASSERT(touchInfo != mTouches.end());
348       if (touchInfo != mTouches.end()) {
349         mTouches.RemoveElementAt(touchInfo);
350       }
351       break;
352     }
353 
354     case MultiTouchInput::MULTITOUCH_CANCEL:
355       // All touches are canceled.
356       mTouches.Clear();
357       break;
358   }
359 }
360 
361 nsTArray<TouchResampler::TouchInfo>::iterator
TouchByIdentifier(int32_t aIdentifier)362 TouchResampler::CurrentTouches::TouchByIdentifier(int32_t aIdentifier) {
363   return std::find_if(mTouches.begin(), mTouches.end(),
364                       [aIdentifier](const TouchInfo& info) {
365                         return info.mIdentifier == aIdentifier;
366                       });
367 }
368 
ResampleTouchPositionAtTime(int32_t aIdentifier,const ScreenIntPoint & aLastObservedPosition,const TimeStamp & aTimeStamp)369 ScreenIntPoint TouchResampler::CurrentTouches::ResampleTouchPositionAtTime(
370     int32_t aIdentifier, const ScreenIntPoint& aLastObservedPosition,
371     const TimeStamp& aTimeStamp) {
372   const auto touchInfo = TouchByIdentifier(aIdentifier);
373   MOZ_ASSERT(touchInfo != mTouches.end());
374   if (touchInfo != mTouches.end()) {
375     return touchInfo->ResampleAtTime(aLastObservedPosition, aTimeStamp);
376   }
377   return aLastObservedPosition;
378 }
379 
380 }  // namespace widget
381 }  // namespace mozilla
382