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/. */
7 #include "TouchResampler.h"
9 #include "nsAlgorithm.h"
12 * TouchResampler implementation
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.
26 // Maximum time between two consecutive data points to consider resampling
28 // Values between 1x and 5x of the touch sampling interval are reasonable.
29 static const double kTouchResampleWindowSize
= 40.0;
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;
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;
47 uint64_t TouchResampler::ProcessEvent(MultiTouchInput
&& aInput
) {
48 mCurrentTouches
.UpdateFromEvent(aInput
);
50 uint64_t eventId
= mNextEventId
;
53 if (aInput
.mType
== MultiTouchInput::MULTITOUCH_MOVE
) {
54 // Touch move events are deferred until NotifyFrame.
55 mDeferredTouchMoveEvents
.push({std::move(aInput
), eventId
});
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();
65 EmitEvent(std::move(aInput
), eventId
);
71 void TouchResampler::NotifyFrame(const TimeStamp
& aTimeStamp
) {
72 TimeStamp lastTouchTime
= mCurrentTouches
.LatestDataPointTime();
73 if (mDeferredTouchMoveEvents
.empty() ||
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();
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();
87 // Clear touch location history so that we don't resample across a pause.
88 mCurrentTouches
.ClearDataPoints();
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
);
99 if (mLastEmittedEventTime
&& sampleTime
< mLastEmittedEventTime
) {
100 // Keep emitted timestamps in order.
101 sampleTime
= mLastEmittedEventTime
;
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
;
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
) {
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
);
128 mOriginalOfResampledTouchMove
= Nothing();
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;
140 resampledPositions
.AppendElement(resampledPosition
);
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
);
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
{
157 touch
.mLocalScreenPoint
,
159 touch
.mRotationAngle
,
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
168 auto futureDataStart
= std::find_if(
169 touch
.mHistoricalData
.begin(), touch
.mHistoricalData
.end(),
171 const SingleTouchData::HistoricalTouchData
& aHistoricalData
) {
172 return aHistoricalData
.mTimeStamp
> sampleTime
;
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
)});
182 touch
.mScreenPoint
= resampledPositions
[i
];
184 input
.mTimeStamp
= sampleTime
;
187 EmitEvent(std::move(input
), eventId
);
188 mInResampledState
= anyPositionDifferentFromOriginal
;
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
);
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
;
211 mRemainingTouchData
.clear();
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();
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.");
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();
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
;
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();
252 PrependLeftoverHistoricalData(&input
);
253 for (auto& touch
: input
.mTouches
) {
254 touch
.mHistoricalData
.RemoveElementsBy([&](const auto& histData
) {
255 return histData
.mTimeStamp
>= input
.mTimeStamp
;
259 EmitExtraEvent(std::move(input
));
260 mInResampledState
= false;
263 void TouchResampler::TouchInfo::Update(const SingleTouchData
& aTouch
,
264 const TimeStamp
& aEventTime
) {
265 for (const auto& historicalData
: aTouch
.mHistoricalData
) {
266 mBaseDataPoint
= mLatestDataPoint
;
268 Some(DataPoint
{historicalData
.mTimeStamp
, historicalData
.mScreenPoint
});
270 mBaseDataPoint
= mLatestDataPoint
;
271 mLatestDataPoint
= Some(DataPoint
{aEventTime
, aTouch
.mScreenPoint
});
274 ScreenIntPoint
TouchResampler::TouchInfo::ResampleAtTime(
275 const ScreenIntPoint
& aLastObservedPosition
, const TimeStamp
& aTimeStamp
) {
277 aTimeStamp
- TimeDuration::FromMilliseconds(kTouchResampleWindowSize
);
278 if (!mBaseDataPoint
|| !mLatestDataPoint
||
279 !(mBaseDataPoint
->mTimeStamp
< mLatestDataPoint
->mTimeStamp
) ||
280 mBaseDataPoint
->mTimeStamp
< cutoff
) {
281 return aLastObservedPosition
;
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
);
290 double x1
= mBaseDataPoint
->mPosition
.x
;
291 double x2
= mLatestDataPoint
->mPosition
.x
;
292 double y1
= mBaseDataPoint
->mPosition
.y
;
293 double y2
= mLatestDataPoint
->mPosition
.y
;
295 int32_t resampledX
= round(x1
+ t
* (x2
- x1
));
296 int32_t resampledY
= round(y1
+ t
* (y2
- y1
));
297 return ScreenIntPoint(resampledX
, resampledY
);
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
);
314 // This is the new touch.
315 newTouches
.AppendElement(TouchInfo
{
316 touch
.mIdentifier
, Nothing(),
317 Some(DataPoint
{aInput
.mTimeStamp
, touch
.mScreenPoint
})});
320 MOZ_ASSERT(mTouches
.IsEmpty(), "Missing touch end before touch start?");
321 mTouches
= std::move(newTouches
);
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
);
335 mLatestDataPointTime
= aInput
.mTimeStamp
;
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
);
350 case MultiTouchInput::MULTITOUCH_CANCEL
:
351 // All touches are canceled.
357 nsTArray
<TouchResampler::TouchInfo
>::iterator
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
;
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
);
373 return aLastObservedPosition
;
376 } // namespace widget
377 } // namespace mozilla