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 {
//
// `samplingOffset` is relative to the current frame time, which
// 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
// frame time.
void sample(Duration samplingOffset) {
void sample(Duration samplingOffset, Duration samplingInterval) {
final SchedulerBinding? scheduler = SchedulerBinding.instance;
assert(scheduler != null);
......@@ -86,10 +88,14 @@ class _Resampler {
// resampling events.
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
// current sample time.
for (final PointerEventResampler resampler in _resamplers.values) {
resampler.sample(sampleTime, _handlePointerEvent);
resampler.sample(sampleTime, nextSampleTime, _handlePointerEvent);
}
// Remove inactive resamplers.
......@@ -138,6 +144,13 @@ class _Resampler {
// 4.666 ms margin is added for this.
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.
///
/// ## Lifecycle of pointer events and the gesture arena
......@@ -257,7 +270,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset);
_resampler.sample(samplingOffset, _samplingInterval);
return;
}
......@@ -388,7 +401,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
void _handleSampleTimeChanged() {
if (!locked) {
if (resamplingEnabled) {
_resampler.sample(samplingOffset);
_resampler.sample(samplingOffset, _samplingInterval);
}
else {
_resampler.stop();
......
......@@ -164,51 +164,44 @@ class PointerEventResampler {
void _dequeueAndSampleNonHoverOrMovePointerEventsUntil(
Duration sampleTime,
Duration nextSampleTime,
HandleEventCallback callback,
) {
while (_queuedEvents.isNotEmpty) {
final PointerEvent event = _queuedEvents.first;
Duration endTime = sampleTime;
// 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`.
if (event.timeStamp > sampleTime) {
// Stop if event is not up or removed. Otherwise, continue to
// allow early processing of up and remove events as this improves
// resampling of these events, which is important for fling
// animations.
if (event is! PointerUpEvent && event is! PointerRemovedEvent) {
// Definitely stop if more recent than `nextSampleTime`.
if (event.timeStamp >= nextSampleTime) {
break;
}
// When this line is reached, the following two invariants hold:
// (1) `event.timeStamp > sampleTime`
// (2) `_next` has the smallest time stamp that's no less than
// `sampleTime`
//
// Therefore, event must satisfy `event.timeStamp >= _next.timeStamp`.
//
// Those events with the minimum `event.timeStamp == _next.timeStamp`
// time stamp are processed early for smoother fling. For events with
// `event.timeStamp > _next.timeStamp`, the following lines break the
// 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) {
// Update `endTime` to allow early processing of up and removed
// events as this improves resampling of these events, which is
// important for fling animations.
if (event is PointerUpEvent || event is PointerRemovedEvent) {
endTime = event.timeStamp;
continue;
}
// Stop if event is not move or hover.
if (event is! PointerMoveEvent && event is! PointerHoverEvent) {
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 wasDown = _isDown;
......@@ -285,11 +278,19 @@ class PointerEventResampler {
/// state that has changed since last sample.
///
/// 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);
// Dequeue and sample pointer events until `sampleTime`.
_dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, callback);
_dequeueAndSampleNonHoverOrMovePointerEventsUntil(sampleTime, nextSampleTime, callback);
// Dispatch resampled pointer location event if tracked.
if (_isTracked) {
......
......@@ -30,32 +30,37 @@ void main() {
ui.PointerData(
change: ui.PointerChange.down,
physicalX: 0.0,
timeStamp: epoch + const Duration(milliseconds: 1),
timeStamp: epoch + const Duration(milliseconds: 10),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 10.0,
timeStamp: epoch + const Duration(milliseconds: 2),
timeStamp: epoch + const Duration(milliseconds: 20),
),
ui.PointerData(
change: ui.PointerChange.move,
physicalX: 20.0,
timeStamp: epoch + const Duration(milliseconds: 3),
timeStamp: epoch + const Duration(milliseconds: 30),
),
ui.PointerData(
change: ui.PointerChange.move,
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(
change: ui.PointerChange.up,
physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 5),
timeStamp: epoch + const Duration(milliseconds: 60),
),
ui.PointerData(
change: ui.PointerChange.remove,
physicalX: 40.0,
timeStamp: epoch + const Duration(milliseconds: 6),
timeStamp: epoch + const Duration(milliseconds: 70),
),
],
);
......@@ -74,27 +79,27 @@ void main() {
);
GestureBinding.instance!.resamplingEnabled = true;
const Duration kSamplingOffset = Duration(microseconds: -5500);
const Duration kSamplingOffset = Duration(milliseconds: -5);
GestureBinding.instance!.samplingOffset = kSamplingOffset;
ui.window.onPointerDataPacket!(packet);
expect(events.length, 0);
await tester.pump(const Duration(milliseconds: 7));
await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 1);
expect(events[0], isA<PointerDownEvent>());
expect(events[0].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[0].position, Offset(5.0 / ui.window.devicePixelRatio, 0.0));
// Now the system time is epoch + 9ms
await tester.pump(const Duration(milliseconds: 2));
// Now the system time is epoch + 40ms
await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 2);
expect(events[1].timeStamp, currentTestFrameTime() + kSamplingOffset);
expect(events[1], isA<PointerMoveEvent>());
expect(events[1].position, Offset(25.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
await tester.pump(const Duration(milliseconds: 2));
// Now the system time is epoch + 60ms
await tester.pump(const Duration(milliseconds: 20));
expect(events.length, 4);
expect(events[2].timeStamp, currentTestFrameTime() + kSamplingOffset);
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