Unverified Commit 4aad058a authored by David Reveman's avatar David Reveman Committed by GitHub

Improve resampling of up and remove events. (#69096)

* Improve resampling of up and remove events.

This improves resampling of these events by searching
for them until the next approximate sample time.
Co-authored-by: 's avatarDavid Reveman <reveman@google.com>
parent d306c37b
...@@ -74,9 +74,11 @@ class _Resampler { ...@@ -74,9 +74,11 @@ class _Resampler {
// //
// `samplingOffset` is relative to the current frame time, which // `samplingOffset` is relative to the current frame time, which
// can be in the past when we're not actively resampling. // can be in the past when we're not actively resampling.
// `samplingInterval` is used to determine the approximate next
// time for resampling.
// `currentSystemFrameTimeStamp` is used to determine the current // `currentSystemFrameTimeStamp` is used to determine the current
// frame time. // frame time.
void sample(Duration samplingOffset) { void sample(Duration samplingOffset, Duration samplingInterval) {
final SchedulerBinding? scheduler = SchedulerBinding.instance; final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null); assert(scheduler != null);
...@@ -86,10 +88,14 @@ class _Resampler { ...@@ -86,10 +88,14 @@ class _Resampler {
// resampling events. // resampling events.
final Duration sampleTime = _frameTime + samplingOffset; final Duration sampleTime = _frameTime + samplingOffset;
// Determine next sample time by adding the sampling interval
// to the current sample time.
final Duration nextSampleTime = sampleTime + samplingInterval;
// Iterate over active resamplers and sample pointer events for // Iterate over active resamplers and sample pointer events for
// current sample time. // current sample time.
for (final PointerEventResampler resampler in _resamplers.values) { for (final PointerEventResampler resampler in _resamplers.values) {
resampler.sample(sampleTime, _handlePointerEvent); resampler.sample(sampleTime, nextSampleTime, _handlePointerEvent);
} }
// Remove inactive resamplers. // Remove inactive resamplers.
...@@ -138,6 +144,13 @@ class _Resampler { ...@@ -138,6 +144,13 @@ class _Resampler {
// 4.666 ms margin is added for this. // 4.666 ms margin is added for this.
const Duration _defaultSamplingOffset = Duration(milliseconds: -38); const Duration _defaultSamplingOffset = Duration(milliseconds: -38);
// The sampling interval.
//
// Sampling interval is used to determine the approximate time for subsequent
// sampling. This is used to decide if early processing of up and removed events
// is appropriate. 16667 us for 60hz sampling interval.
const Duration _samplingInterval = Duration(microseconds: 16667);
/// A binding for the gesture subsystem. /// A binding for the gesture subsystem.
/// ///
/// ## Lifecycle of pointer events and the gesture arena /// ## Lifecycle of pointer events and the gesture arena
...@@ -257,7 +270,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -257,7 +270,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
if (resamplingEnabled) { if (resamplingEnabled) {
_resampler.addOrDispatch(event); _resampler.addOrDispatch(event);
_resampler.sample(samplingOffset); _resampler.sample(samplingOffset, _samplingInterval);
return; return;
} }
...@@ -388,7 +401,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H ...@@ -388,7 +401,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handleSampleTimeChanged() { void _handleSampleTimeChanged() {
if (!locked) { if (!locked) {
if (resamplingEnabled) { if (resamplingEnabled) {
_resampler.sample(samplingOffset); _resampler.sample(samplingOffset, _samplingInterval);
} }
else { else {
_resampler.stop(); _resampler.stop();
......
...@@ -164,51 +164,44 @@ class PointerEventResampler { ...@@ -164,51 +164,44 @@ class PointerEventResampler {
void _dequeueAndSampleNonHoverOrMovePointerEventsUntil( void _dequeueAndSampleNonHoverOrMovePointerEventsUntil(
Duration sampleTime, Duration sampleTime,
Duration nextSampleTime,
HandleEventCallback callback, HandleEventCallback callback,
) { ) {
while (_queuedEvents.isNotEmpty) { Duration endTime = sampleTime;
final PointerEvent event = _queuedEvents.first; // Scan queued events to determine end time.
final Iterator<PointerEvent> it = _queuedEvents.iterator;
while (it.moveNext()) {
final PointerEvent event = it.current;
// Potentially stop dispatching events if more recent than `sampleTime`. // Potentially stop dispatching events if more recent than `sampleTime`.
if (event.timeStamp > sampleTime) { if (event.timeStamp > sampleTime) {
// Stop if event is not up or removed. Otherwise, continue to // Definitely stop if more recent than `nextSampleTime`.
// allow early processing of up and remove events as this improves if (event.timeStamp >= nextSampleTime) {
// resampling of these events, which is important for fling
// animations.
if (event is! PointerUpEvent && event is! PointerRemovedEvent) {
break; break;
} }
// When this line is reached, the following two invariants hold: // Update `endTime` to allow early processing of up and removed
// (1) `event.timeStamp > sampleTime` // events as this improves resampling of these events, which is
// (2) `_next` has the smallest time stamp that's no less than // important for fling animations.
// `sampleTime` if (event is PointerUpEvent || event is PointerRemovedEvent) {
// endTime = event.timeStamp;
// Therefore, event must satisfy `event.timeStamp >= _next.timeStamp`. continue;
// }
// Those events with the minimum `event.timeStamp == _next.timeStamp`
// time stamp are processed early for smoother fling. For events with // Stop if event is not move or hover.
// `event.timeStamp > _next.timeStamp`, the following lines break the if (event is! PointerMoveEvent && event is! PointerHoverEvent) {
// while loop to stop the early processing.
//
// Specifically, when `sampleTime < _next.timeStamp`, there must be
// at least one event with `_next.timeStamp == event.timeStamp`
// and that event is `_next` itself, and it will be processed early.
//
// When `sampleTime == _next.timeStamp`, all events with
// `event.timeStamp > sampleTime` must also have
// `event.timeStamp > _next.timeStamp` so no events will be processed
// early.
//
// When the input frequency is no greater than the sampling
// frequency, this early processing should guarantee that `up` and
// `remove` events are always re-sampled.
final Duration nextTimeStamp = _next?.timeStamp ?? Duration.zero;
assert(event.timeStamp >= nextTimeStamp);
if (event.timeStamp > nextTimeStamp) {
break; break;
} }
} }
}
while (_queuedEvents.isNotEmpty) {
final PointerEvent event = _queuedEvents.first;
// Stop dispatching events if more recent than `endTime`.
if (event.timeStamp > endTime) {
break;
}
final bool wasTracked = _isTracked; final bool wasTracked = _isTracked;
final bool wasDown = _isDown; final bool wasDown = _isDown;
...@@ -285,11 +278,19 @@ class PointerEventResampler { ...@@ -285,11 +278,19 @@ class PointerEventResampler {
/// state that has changed since last sample. /// state that has changed since last sample.
/// ///
/// Calling [callback] must not add or sample events. /// Calling [callback] must not add or sample events.
void sample(Duration sampleTime, HandleEventCallback callback) { ///
/// Positive value for `nextSampleTime` allow early processing of
/// up and removed events. This improves resampling of these events,
/// which is important for fling animations.
void sample(
Duration sampleTime,
Duration nextSampleTime,
HandleEventCallback callback,
) {
_processPointerEvents(sampleTime); _processPointerEvents(sampleTime);
// Dequeue and sample pointer events until `sampleTime`. // Dequeue and sample pointer events until `sampleTime`.
_dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, callback); _dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, nextSampleTime, callback);
// Dispatch resampled pointer location event if tracked. // Dispatch resampled pointer location event if tracked.
if (_isTracked) { if (_isTracked) {
......
...@@ -30,32 +30,37 @@ void main() { ...@@ -30,32 +30,37 @@ void main() {
ui.PointerData( ui.PointerData(
change: ui.PointerChange.down, change: ui.PointerChange.down,
physicalX: 0.0, physicalX: 0.0,
timeStamp: epoch + const Duration(milliseconds: 1), timeStamp: epoch + const Duration(milliseconds: 10),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 10.0, physicalX: 10.0,
timeStamp: epoch + const Duration(milliseconds: 2), timeStamp: epoch + const Duration(milliseconds: 20),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 20.0, physicalX: 20.0,
timeStamp: epoch + const Duration(milliseconds: 3), timeStamp: epoch + const Duration(milliseconds: 30),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.move, change: ui.PointerChange.move,
physicalX: 30.0, physicalX: 30.0,
timeStamp: epoch + const Duration(milliseconds: 4), timeStamp: epoch + const Duration(milliseconds: 40),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 50),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.up, change: ui.PointerChange.up,
physicalX: 40.0, physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 5), timeStamp: epoch + const Duration(milliseconds: 60),
), ),
ui.PointerData( ui.PointerData(
change: ui.PointerChange.remove, change: ui.PointerChange.remove,
physicalX: 40.0, physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 6), timeStamp: epoch + const Duration(milliseconds: 70),
), ),
], ],
); );
...@@ -74,27 +79,27 @@ void main() { ...@@ -74,27 +79,27 @@ void main() {
); );
GestureBinding.instance!.resamplingEnabled = true; GestureBinding.instance!.resamplingEnabled = true;
const Duration kSamplingOffset = Duration(microseconds: -5500); const Duration kSamplingOffset = Duration(milliseconds: -5);
GestureBinding.instance!.samplingOffset = kSamplingOffset; GestureBinding.instance!.samplingOffset = kSamplingOffset;
ui.window.onPointerDataPacket!(packet); ui.window.onPointerDataPacket!(packet);
expect(events.length, 0); expect(events.length, 0);
await tester.pump(const Duration(milliseconds: 7)); await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 1); expect(events.length, 1);
expect(events[0], isA<PointerDownEvent>()); expect(events[0], isA<PointerDownEvent>());
expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0)); expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0));
// Now the system time is epoch + 9ms // Now the system time is epoch + 40ms
await tester.pump(const Duration(milliseconds: 2)); await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 2); expect(events.length, 2);
expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[1], isA<PointerMoveEvent>()); expect(events[1], isA<PointerMoveEvent>());
expect(events[1].position, Offset(25.0 / ui.window.devicePixelRatio, 0.0)); expect(events[1].position, Offset(25.0 / ui.window.devicePixelRatio, 0.0));
expect(events[1].delta, Offset(20.0 / ui.window.devicePixelRatio, 0.0)); expect(events[1].delta, Offset(20.0 / ui.window.devicePixelRatio, 0.0));
// Now the system time is epoch + 11ms // Now the system time is epoch + 60ms
await tester.pump(const Duration(milliseconds: 2)); await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 4); expect(events.length, 4);
expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset); expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[2], isA<PointerMoveEvent>()); expect(events[2], isA<PointerMoveEvent>());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment