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     std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front());
116     mDeferredTouchMoveEvents.pop();
117     if (mDeferredTouchMoveEvents.empty() || input.mTimeStamp >= sampleTime) {
118       break;
119     }
120     // Flush this event to the outgoing queue without resampling. What ends up
121     // on the screen will still be smooth because we will proceed to emit a
122     // resampled event before the paint for this frame starts.
123     PrependLeftoverHistoricalData(&input);
124     MOZ_RELEASE_ASSERT(input.mTimeStamp < sampleTime);
125     EmitEvent(std::move(input), eventId);
126   }
127 
128   mOriginalOfResampledTouchMove = Nothing();
129 
130   // Compute the resampled touch positions.
131   nsTArray<ScreenIntPoint> resampledPositions;
132   bool anyPositionDifferentFromOriginal = false;
133   for (const auto& touch : input.mTouches) {
134     ScreenIntPoint resampledPosition =
135         mCurrentTouches.ResampleTouchPositionAtTime(
136             touch.mIdentifier, touch.mScreenPoint, sampleTime);
137     if (resampledPosition != touch.mScreenPoint) {
138       anyPositionDifferentFromOriginal = true;
139     }
140     resampledPositions.AppendElement(resampledPosition);
141   }
142 
143   if (anyPositionDifferentFromOriginal) {
144     // Store a copy of the original event, so that we can return to an
145     // non-resampled position later, if necessary.
146     mOriginalOfResampledTouchMove = Some(input);
147 
148     // Add the original observed position to the historical data, as well as any
149     // leftover historical positions from the previous touch move event, and
150     // store the resampled values in the "final" position of the event.
151     PrependLeftoverHistoricalData(&input);
152     for (size_t i = 0; i < input.mTouches.Length(); i++) {
153       auto& touch = input.mTouches[i];
154       touch.mHistoricalData.AppendElement(SingleTouchData::HistoricalTouchData{
155           input.mTimeStamp,
156           touch.mScreenPoint,
157           touch.mLocalScreenPoint,
158           touch.mRadius,
159           touch.mRotationAngle,
160           touch.mForce,
161       });
162 
163       // Remove any historical touch data that's in the future, compared to
164       // sampleTime. This data will be included by upcoming touch move
165       // events. This only happens if the frame timestamp can be older than the
166       // event timestamp, i.e. if interpolation occurs (rather than
167       // extrapolation).
168       auto futureDataStart = std::find_if(
169           touch.mHistoricalData.begin(), touch.mHistoricalData.end(),
170           [sampleTime](
171               const SingleTouchData::HistoricalTouchData& aHistoricalData) {
172             return aHistoricalData.mTimeStamp > sampleTime;
173           });
174       if (futureDataStart != touch.mHistoricalData.end()) {
175         nsTArray<SingleTouchData::HistoricalTouchData> futureData(
176             Span<SingleTouchData::HistoricalTouchData>(touch.mHistoricalData)
177                 .From(futureDataStart.GetIndex()));
178         touch.mHistoricalData.TruncateLength(futureDataStart.GetIndex());
179         mRemainingTouchData.insert({touch.mIdentifier, std::move(futureData)});
180       }
181 
182       touch.mScreenPoint = resampledPositions[i];
183     }
184     input.mTimeStamp = sampleTime;
185   }
186 
187   EmitEvent(std::move(input), eventId);
188   mInResampledState = anyPositionDifferentFromOriginal;
189 }
190 
PrependLeftoverHistoricalData(MultiTouchInput * aInput)191 void TouchResampler::PrependLeftoverHistoricalData(MultiTouchInput* aInput) {
192   for (auto& touch : aInput->mTouches) {
193     auto leftoverData = mRemainingTouchData.find(touch.mIdentifier);
194     if (leftoverData != mRemainingTouchData.end()) {
195       nsTArray<SingleTouchData::HistoricalTouchData> data =
196           std::move(leftoverData->second);
197       mRemainingTouchData.erase(leftoverData);
198       touch.mHistoricalData.InsertElementsAt(0, data);
199     }
200 
201     if (TimeStamp cutoffTime = mLastEmittedEventTime) {
202       // If we received historical touch data that was further in the past than
203       // the last resampled event, discard that data so that the touch data
204       // points are emitted in order.
205       touch.mHistoricalData.RemoveElementsBy(
206           [cutoffTime](const SingleTouchData::HistoricalTouchData& aTouchData) {
207             return aTouchData.mTimeStamp < cutoffTime;
208           });
209     }
210   }
211   mRemainingTouchData.clear();
212 }
213 
FlushDeferredTouchMoveEventsUnresampled()214 void TouchResampler::FlushDeferredTouchMoveEventsUnresampled() {
215   while (!mDeferredTouchMoveEvents.empty()) {
216     auto [input, eventId] = std::move(mDeferredTouchMoveEvents.front());
217     mDeferredTouchMoveEvents.pop();
218     PrependLeftoverHistoricalData(&input);
219     EmitEvent(std::move(input), eventId);
220     mInResampledState = false;
221     mOriginalOfResampledTouchMove = Nothing();
222   }
223 }
224 
ReturnToNonResampledState()225 void TouchResampler::ReturnToNonResampledState() {
226   MOZ_RELEASE_ASSERT(mInResampledState);
227   MOZ_RELEASE_ASSERT(mDeferredTouchMoveEvents.empty(),
228                      "Don't call this if there is a deferred touch move event. "
229                      "We can return to the non-resampled state by sending that "
230                      "event, rather than a copy of a previous event.");
231 
232   // The last outgoing event was a resampled touch move event.
233   // Return to the non-resampled state, by sending a touch move event to
234   // "overwrite" any resampled positions with the original observed positions.
235   MultiTouchInput input = std::move(*mOriginalOfResampledTouchMove);
236   mOriginalOfResampledTouchMove = Nothing();
237 
238   // For the event's timestamp, we want to backdate the correction as far as we
239   // can, while still preserving timestamp ordering. But we also don't want to
240   // backdate it to be older than it was originally.
241   if (mLastEmittedEventTime > input.mTimeStamp) {
242     input.mTimeStamp = mLastEmittedEventTime;
243   }
244 
245   // Assemble the correct historical touch data for this event.
246   // We don't want to include data points that we've already sent out with the
247   // resampled event. And from the leftover data points, we only want those that
248   // don't duplicate the final time + position of this event.
249   for (auto& touch : input.mTouches) {
250     touch.mHistoricalData.Clear();
251   }
252   PrependLeftoverHistoricalData(&input);
253   for (auto& touch : input.mTouches) {
254     touch.mHistoricalData.RemoveElementsBy([&](const auto& histData) {
255       return histData.mTimeStamp >= input.mTimeStamp;
256     });
257   }
258 
259   EmitExtraEvent(std::move(input));
260   mInResampledState = false;
261 }
262 
Update(const SingleTouchData & aTouch,const TimeStamp & aEventTime)263 void TouchResampler::TouchInfo::Update(const SingleTouchData& aTouch,
264                                        const TimeStamp& aEventTime) {
265   for (const auto& historicalData : aTouch.mHistoricalData) {
266     mBaseDataPoint = mLatestDataPoint;
267     mLatestDataPoint =
268         Some(DataPoint{historicalData.mTimeStamp, historicalData.mScreenPoint});
269   }
270   mBaseDataPoint = mLatestDataPoint;
271   mLatestDataPoint = Some(DataPoint{aEventTime, aTouch.mScreenPoint});
272 }
273 
ResampleAtTime(const ScreenIntPoint & aLastObservedPosition,const TimeStamp & aTimeStamp)274 ScreenIntPoint TouchResampler::TouchInfo::ResampleAtTime(
275     const ScreenIntPoint& aLastObservedPosition, const TimeStamp& aTimeStamp) {
276   TimeStamp cutoff =
277       aTimeStamp - TimeDuration::FromMilliseconds(kTouchResampleWindowSize);
278   if (!mBaseDataPoint || !mLatestDataPoint ||
279       !(mBaseDataPoint->mTimeStamp < mLatestDataPoint->mTimeStamp) ||
280       mBaseDataPoint->mTimeStamp < cutoff) {
281     return aLastObservedPosition;
282   }
283 
284   // For the actual resampling, connect the last two data points with a line and
285   // sample along that line.
286   TimeStamp t1 = mBaseDataPoint->mTimeStamp;
287   TimeStamp t2 = mLatestDataPoint->mTimeStamp;
288   double t = (aTimeStamp - t1) / (t2 - t1);
289 
290   double x1 = mBaseDataPoint->mPosition.x;
291   double x2 = mLatestDataPoint->mPosition.x;
292   double y1 = mBaseDataPoint->mPosition.y;
293   double y2 = mLatestDataPoint->mPosition.y;
294 
295   int32_t resampledX = round(x1 + t * (x2 - x1));
296   int32_t resampledY = round(y1 + t * (y2 - y1));
297   return ScreenIntPoint(resampledX, resampledY);
298 }
299 
UpdateFromEvent(const MultiTouchInput & aInput)300 void TouchResampler::CurrentTouches::UpdateFromEvent(
301     const MultiTouchInput& aInput) {
302   switch (aInput.mType) {
303     case MultiTouchInput::MULTITOUCH_START: {
304       // A new touch has been added; make sure mTouches reflects the current
305       // touches in the event.
306       nsTArray<TouchInfo> newTouches;
307       for (const auto& touch : aInput.mTouches) {
308         const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
309         if (touchInfo != mTouches.end()) {
310           // This is one of the existing touches.
311           newTouches.AppendElement(std::move(*touchInfo));
312           mTouches.RemoveElementAt(touchInfo);
313         } else {
314           // This is the new touch.
315           newTouches.AppendElement(TouchInfo{
316               touch.mIdentifier, Nothing(),
317               Some(DataPoint{aInput.mTimeStamp, touch.mScreenPoint})});
318         }
319       }
320       MOZ_ASSERT(mTouches.IsEmpty(), "Missing touch end before touch start?");
321       mTouches = std::move(newTouches);
322       break;
323     }
324 
325     case MultiTouchInput::MULTITOUCH_MOVE: {
326       // The touches have moved.
327       // Add position information to the history data points.
328       for (const auto& touch : aInput.mTouches) {
329         const auto touchInfo = TouchByIdentifier(touch.mIdentifier);
330         MOZ_ASSERT(touchInfo != mTouches.end());
331         if (touchInfo != mTouches.end()) {
332           touchInfo->Update(touch, aInput.mTimeStamp);
333         }
334       }
335       mLatestDataPointTime = aInput.mTimeStamp;
336       break;
337     }
338 
339     case MultiTouchInput::MULTITOUCH_END: {
340       // A touch has been removed.
341       MOZ_RELEASE_ASSERT(aInput.mTouches.Length() == 1);
342       const auto touchInfo = TouchByIdentifier(aInput.mTouches[0].mIdentifier);
343       MOZ_ASSERT(touchInfo != mTouches.end());
344       if (touchInfo != mTouches.end()) {
345         mTouches.RemoveElementAt(touchInfo);
346       }
347       break;
348     }
349 
350     case MultiTouchInput::MULTITOUCH_CANCEL:
351       // All touches are canceled.
352       mTouches.Clear();
353       break;
354   }
355 }
356 
357 nsTArray<TouchResampler::TouchInfo>::iterator
TouchByIdentifier(int32_t aIdentifier)358 TouchResampler::CurrentTouches::TouchByIdentifier(int32_t aIdentifier) {
359   return std::find_if(mTouches.begin(), mTouches.end(),
360                       [aIdentifier](const TouchInfo& info) {
361                         return info.mIdentifier == aIdentifier;
362                       });
363 }
364 
ResampleTouchPositionAtTime(int32_t aIdentifier,const ScreenIntPoint & aLastObservedPosition,const TimeStamp & aTimeStamp)365 ScreenIntPoint TouchResampler::CurrentTouches::ResampleTouchPositionAtTime(
366     int32_t aIdentifier, const ScreenIntPoint& aLastObservedPosition,
367     const TimeStamp& aTimeStamp) {
368   const auto touchInfo = TouchByIdentifier(aIdentifier);
369   MOZ_ASSERT(touchInfo != mTouches.end());
370   if (touchInfo != mTouches.end()) {
371     return touchInfo->ResampleAtTime(aLastObservedPosition, aTimeStamp);
372   }
373   return aLastObservedPosition;
374 }
375 
376 }  // namespace widget
377 }  // namespace mozilla
378