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