/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TouchResampler.h" #include "nsAlgorithm.h" /** * TouchResampler implementation */ namespace mozilla { namespace widget { // The values below have been tested and found to be acceptable on a device // with a display refresh rate of 60Hz and touch sampling rate of 100Hz. // While their "ideal" values are dependent on the exact rates of each device, // the values we've picked below should be somewhat robust across a variation of // different rates. They mostly aim to avoid making predictions that are too far // away (in terms of distance) from the finger, and to detect pauses in the // finger motion without too much delay. // Maximum time between two consecutive data points to consider resampling // between them. // Values between 1x and 5x of the touch sampling interval are reasonable. static const double kTouchResampleWindowSize = 40.0; // These next two values constrain the sampling timestamp. // Our caller will usually adjust frame timestamps to be slightly in the past, // for example by 5ms. This means that, during normal operation, we will // maximally need to predict by [touch sampling rate] minus 5ms. // So we would like kTouchResampleMaxPredictMs to satisfy the following: // kTouchResampleMaxPredictMs + [frame time adjust] > [touch sampling rate] static const double kTouchResampleMaxPredictMs = 8.0; // This one is a protection against very outdated frame timestamps. // Values larger than the touch sampling interval and less than 3x of the vsync // interval are reasonable. static const double kTouchResampleMaxBacksampleMs = 20.0; // The maximum age of the most recent data point to consider resampling. // Should be between 1x and 3x of the touch sampling interval. static const double kTouchResampleOldTouchThresholdMs = 17.0; uint64_t TouchResampler::ProcessEvent(MultiTouchInput&& aInput) { mCurrentTouches.UpdateFromEvent(aInput); uint64_t eventId = mNextEventId; mNextEventId++; if (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) { // Touch move events are deferred until NotifyFrame. mDeferredTouchMoveEvents.push({std::move(aInput), eventId}); } else { // Non-move events are transferred to the outgoing queue unmodified. // If there are pending touch move events, flush those out first, so that // events are emitted in the right order. FlushDeferredTouchMoveEventsUnresampled(); if (mInResampledState) { // Return to a non-resampled state before emitting a non-move event. ReturnToNonResampledState(); } EmitEvent(std::move(aInput), eventId); } return eventId; } void TouchResampler::NotifyFrame(const TimeStamp& aTimeStamp) { TimeStamp lastTouchTime = mCurrentTouches.LatestDataPointTime(); if (mDeferredTouchMoveEvents.empty() || (lastTouchTime && lastTouchTime < aTimeStamp - TimeDuration::FromMilliseconds( kTouchResampleOldTouchThresholdMs))) { // We haven't received a touch move event in a while, so the fingers must // have stopped moving. Flush any old touch move events. FlushDeferredTouchMoveEventsUnresampled(); if (mInResampledState) { // Make sure we pause at the resting position that we actually observed, // and not at a resampled position. ReturnToNonResampledState(); } // Clear touch location history so that we don't resample across a pause. mCurrentTouches.ClearDataPoints(); return; } MOZ_RELEASE_ASSERT(lastTouchTime); TimeStamp lowerBound = lastTouchTime - TimeDuration::FromMilliseconds( kTouchResampleMaxBacksampleMs); TimeStamp upperBound = lastTouchTime + TimeDuration::FromMilliseconds( kTouchResampleMaxPredictMs); TimeStamp sampleTime = clamped(aTimeStamp, lowerBound, upperBound); if (mLastEmittedEventTime && sampleTime < mLastEmittedEventTime) { // Keep emitted timestamps in order. sampleTime = mLastEmittedEventTime; } // We have at least one pending touch move event. Pick one of the events from // mDeferredTouchMoveEvents as the base event for the resampling adjustment. // We want to produce an event stream whose timestamps are in the right order. // As the base event, use the first event that's at or after sampleTime, // unless there is no such event, in that case use the last one we have. We // will set the timestamp on the resampled event to sampleTime later. // Flush out any older events so that everything remains in the right order. MultiTouchInput input; uint64_t eventId; while (true) { MOZ_RELEASE_ASSERT(!mDeferredTouchMoveEvents.empty()); std::tie(input, eventId) = std::move(mDeferredTouchMoveEvents.front()); mDeferredTouchMoveEvents.pop(); if (mDeferredTouchMoveEvents.empty() || input.mTimeStamp >= sampleTime) { break; } // Flush this event to the outgoing queue without resampling. What ends up // on the screen will still be smooth because we will proceed to emit a // resampled event before the paint for this frame starts. PrependLeftoverHistoricalData(&input); MOZ_RELEASE_ASSERT(input.mTimeStamp < sampleTime); EmitEvent(std::move(input), eventId); } mOriginalOfResampledTouchMove = Nothing(); // Compute the resampled touch positions. nsTArray resampledPositions; bool anyPositionDifferentFromOriginal = false; for (const auto& touch : input.mTouches) { ScreenIntPoint resampledPosition = mCurrentTouches.ResampleTouchPositionAtTime( touch.mIdentifier, touch.mScreenPoint, sampleTime); if (resampledPosition != touch.mScreenPoint) { anyPositionDifferentFromOriginal = true; } resampledPositions.AppendElement(resampledPosition); } if (anyPositionDifferentFromOriginal) { // Store a copy of the original event, so that we can return to an // non-resampled position later, if necessary. mOriginalOfResampledTouchMove = Some(input); // Add the original observed position to the historical data, as well as any // leftover historical positions from the previous touch move event, and // store the resampled values in the "final" position of the event. PrependLeftoverHistoricalData(&input); for (size_t i = 0; i < input.mTouches.Length(); i++) { auto& touch = input.mTouches[i]; touch.mHistoricalData.AppendElement(SingleTouchData::HistoricalTouchData{ input.mTimeStamp, touch.mScreenPoint, touch.mLocalScreenPoint, touch.mRadius, touch.mRotationAngle, touch.mForce, }); // Remove any historical touch data that's in the future, compared to // sampleTime. This data will be included by upcoming touch move // events. This only happens if the frame timestamp can be older than the // event timestamp, i.e. if interpolation occurs (rather than // extrapolation). auto futureDataStart = std::find_if( touch.mHistoricalData.begin(), touch.mHistoricalData.end(), [sampleTime]( const SingleTouchData::HistoricalTouchData& aHistoricalData) { return aHistoricalData.mTimeStamp > sampleTime; }); if (futureDataStart != touch.mHistoricalData.end()) { nsTArray futureData( Span(touch.mHistoricalData) .From(futureDataStart.GetIndex())); touch.mHistoricalData.TruncateLength(futureDataStart.GetIndex()); mRemainingTouchData.insert({touch.mIdentifier, std::move(futureData)}); } touch.mScreenPoint = resampledPositions[i]; } input.mTimeStamp = sampleTime; } EmitEvent(std::move(input), eventId); mInResampledState = anyPositionDifferentFromOriginal; } void TouchResampler::PrependLeftoverHistoricalData(MultiTouchInput* aInput) { for (auto& touch : aInput->mTouches) { auto leftoverData = mRemainingTouchData.find(touch.mIdentifier); if (leftoverData != mRemainingTouchData.end()) { nsTArray data = std::move(leftoverData->second); mRemainingTouchData.erase(leftoverData); touch.mHistoricalData.InsertElementsAt(0, data); } if (TimeStamp cutoffTime = mLastEmittedEventTime) { // If we received historical touch data that was further in the past than // the last resampled event, discard that data so that the touch data // points are emitted in order. touch.mHistoricalData.RemoveElementsBy( [cutoffTime](const SingleTouchData::HistoricalTouchData& aTouchData) { return aTouchData.mTimeStamp < cutoffTime; }); } } mRemainingTouchData.clear(); } void TouchResampler::FlushDeferredTouchMoveEventsUnresampled() { while (!mDeferredTouchMoveEvents.empty()) { auto [input, eventId] = std::move(mDeferredTouchMoveEvents.front()); mDeferredTouchMoveEvents.pop(); PrependLeftoverHistoricalData(&input); EmitEvent(std::move(input), eventId); mInResampledState = false; mOriginalOfResampledTouchMove = Nothing(); } } void TouchResampler::ReturnToNonResampledState() { MOZ_RELEASE_ASSERT(mInResampledState); MOZ_RELEASE_ASSERT(mDeferredTouchMoveEvents.empty(), "Don't call this if there is a deferred touch move event. " "We can return to the non-resampled state by sending that " "event, rather than a copy of a previous event."); // The last outgoing event was a resampled touch move event. // Return to the non-resampled state, by sending a touch move event to // "overwrite" any resampled positions with the original observed positions. MultiTouchInput input = std::move(*mOriginalOfResampledTouchMove); mOriginalOfResampledTouchMove = Nothing(); // For the event's timestamp, we want to backdate the correction as far as we // can, while still preserving timestamp ordering. But we also don't want to // backdate it to be older than it was originally. if (mLastEmittedEventTime > input.mTimeStamp) { input.mTimeStamp = mLastEmittedEventTime; } // Assemble the correct historical touch data for this event. // We don't want to include data points that we've already sent out with the // resampled event. And from the leftover data points, we only want those that // don't duplicate the final time + position of this event. for (auto& touch : input.mTouches) { touch.mHistoricalData.Clear(); } PrependLeftoverHistoricalData(&input); for (auto& touch : input.mTouches) { touch.mHistoricalData.RemoveElementsBy([&](const auto& histData) { return histData.mTimeStamp >= input.mTimeStamp; }); } EmitExtraEvent(std::move(input)); mInResampledState = false; } void TouchResampler::TouchInfo::Update(const SingleTouchData& aTouch, const TimeStamp& aEventTime) { for (const auto& historicalData : aTouch.mHistoricalData) { mBaseDataPoint = mLatestDataPoint; mLatestDataPoint = Some(DataPoint{historicalData.mTimeStamp, historicalData.mScreenPoint}); } mBaseDataPoint = mLatestDataPoint; mLatestDataPoint = Some(DataPoint{aEventTime, aTouch.mScreenPoint}); } ScreenIntPoint TouchResampler::TouchInfo::ResampleAtTime( const ScreenIntPoint& aLastObservedPosition, const TimeStamp& aTimeStamp) { TimeStamp cutoff = aTimeStamp - TimeDuration::FromMilliseconds(kTouchResampleWindowSize); if (!mBaseDataPoint || !mLatestDataPoint || !(mBaseDataPoint->mTimeStamp < mLatestDataPoint->mTimeStamp) || mBaseDataPoint->mTimeStamp < cutoff) { return aLastObservedPosition; } // For the actual resampling, connect the last two data points with a line and // sample along that line. TimeStamp t1 = mBaseDataPoint->mTimeStamp; TimeStamp t2 = mLatestDataPoint->mTimeStamp; double t = (aTimeStamp - t1) / (t2 - t1); double x1 = mBaseDataPoint->mPosition.x; double x2 = mLatestDataPoint->mPosition.x; double y1 = mBaseDataPoint->mPosition.y; double y2 = mLatestDataPoint->mPosition.y; int32_t resampledX = round(x1 + t * (x2 - x1)); int32_t resampledY = round(y1 + t * (y2 - y1)); return ScreenIntPoint(resampledX, resampledY); } void TouchResampler::CurrentTouches::UpdateFromEvent( const MultiTouchInput& aInput) { switch (aInput.mType) { case MultiTouchInput::MULTITOUCH_START: { // A new touch has been added; make sure mTouches reflects the current // touches in the event. nsTArray newTouches; for (const auto& touch : aInput.mTouches) { const auto touchInfo = TouchByIdentifier(touch.mIdentifier); if (touchInfo != mTouches.end()) { // This is one of the existing touches. newTouches.AppendElement(std::move(*touchInfo)); mTouches.RemoveElementAt(touchInfo); } else { // This is the new touch. newTouches.AppendElement(TouchInfo{ touch.mIdentifier, Nothing(), Some(DataPoint{aInput.mTimeStamp, touch.mScreenPoint})}); } } MOZ_ASSERT(mTouches.IsEmpty(), "Missing touch end before touch start?"); mTouches = std::move(newTouches); break; } case MultiTouchInput::MULTITOUCH_MOVE: { // The touches have moved. // Add position information to the history data points. for (const auto& touch : aInput.mTouches) { const auto touchInfo = TouchByIdentifier(touch.mIdentifier); MOZ_ASSERT(touchInfo != mTouches.end()); if (touchInfo != mTouches.end()) { touchInfo->Update(touch, aInput.mTimeStamp); } } mLatestDataPointTime = aInput.mTimeStamp; break; } case MultiTouchInput::MULTITOUCH_END: { // A touch has been removed. MOZ_RELEASE_ASSERT(aInput.mTouches.Length() == 1); const auto touchInfo = TouchByIdentifier(aInput.mTouches[0].mIdentifier); MOZ_ASSERT(touchInfo != mTouches.end()); if (touchInfo != mTouches.end()) { mTouches.RemoveElementAt(touchInfo); } break; } case MultiTouchInput::MULTITOUCH_CANCEL: // All touches are canceled. mTouches.Clear(); break; } } nsTArray::iterator TouchResampler::CurrentTouches::TouchByIdentifier(int32_t aIdentifier) { return std::find_if(mTouches.begin(), mTouches.end(), [aIdentifier](const TouchInfo& info) { return info.mIdentifier == aIdentifier; }); } ScreenIntPoint TouchResampler::CurrentTouches::ResampleTouchPositionAtTime( int32_t aIdentifier, const ScreenIntPoint& aLastObservedPosition, const TimeStamp& aTimeStamp) { const auto touchInfo = TouchByIdentifier(aIdentifier); MOZ_ASSERT(touchInfo != mTouches.end()); if (touchInfo != mTouches.end()) { return touchInfo->ResampleAtTime(aLastObservedPosition, aTimeStamp); } return aLastObservedPosition; } } // namespace widget } // namespace mozilla