Bug 1716846 [wpt PR 29402] - Update wpt metadata, a=testonly
[gecko.git] / widget / TouchResampler.cpp
blob51974a440596025290f18a24e96f0c2937cd74b3
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"
11 /**
12 * TouchResampler implementation
15 namespace mozilla {
16 namespace widget {
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
27 // between them.
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;
51 mNextEventId++;
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();
65 EmitEvent(std::move(aInput), eventId);
68 return eventId;
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();
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();
89 return;
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;
112 uint64_t eventId;
113 while (true) {
114 MOZ_RELEASE_ASSERT(!mDeferredTouchMoveEvents.empty());
116 std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front());
117 mDeferredTouchMoveEvents.pop();
118 if (mDeferredTouchMoveEvents.empty() || input.mTimeStamp >= sampleTime) {
119 break;
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);
130 mOriginalOfResampledTouchMove = Nothing();
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;
142 resampledPositions.AppendElement(resampledPosition);
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);
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,
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;
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)});
184 touch.mScreenPoint = resampledPositions[i];
186 input.mTimeStamp = sampleTime;
189 EmitEvent(std::move(input), eventId);
190 mInResampledState = anyPositionDifferentFromOriginal;
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);
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;
213 mRemainingTouchData.clear();
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();
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.");
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();
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;
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();
256 PrependLeftoverHistoricalData(&input);
257 for (auto& touch : input.mTouches) {
258 touch.mHistoricalData.RemoveElementsBy([&](const auto& histData) {
259 return histData.mTimeStamp >= input.mTimeStamp;
263 EmitExtraEvent(std::move(input));
264 mInResampledState = false;
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});
274 mBaseDataPoint = mLatestDataPoint;
275 mLatestDataPoint = Some(DataPoint{aEventTime, aTouch.mScreenPoint});
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;
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);
294 double x1 = mBaseDataPoint->mPosition.x;
295 double x2 = mLatestDataPoint->mPosition.x;
296 double y1 = mBaseDataPoint->mPosition.y;
297 double y2 = mLatestDataPoint->mPosition.y;
299 int32_t resampledX = round(x1 + t * (x2 - x1));
300 int32_t resampledY = round(y1 + t * (y2 - y1));
301 return ScreenIntPoint(resampledX, resampledY);
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})});
324 MOZ_ASSERT(mTouches.IsEmpty(), "Missing touch end before touch start?");
325 mTouches = std::move(newTouches);
326 break;
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);
339 mLatestDataPointTime = aInput.mTimeStamp;
340 break;
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);
351 break;
354 case MultiTouchInput::MULTITOUCH_CANCEL:
355 // All touches are canceled.
356 mTouches.Clear();
357 break;
361 nsTArray<TouchResampler::TouchInfo>::iterator
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;
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);
377 return aLastObservedPosition;
380 } // namespace widget
381 } // namespace mozilla