Add a buffer to smooth mouse scroll acceleration

Sometimes a bluetooth mouse will send multiple scroll events in quick
succession when in reality they were generated more slowly. This can
cause a large spike in scroll velocity. We already make sure that we use
a minimum time delta between events when calculating scroll velocity,
but the issue is still noticieable. Increasing this time delta can help,
but degrades scroll acceleration when the user intends to scorll fast.

In this CL, we add a small buffer of scroll events that we use to
calculate scroll velocity for the purpose of scroll acceleration.  This
allows us to accelerate scrolls appropriately when the user is
consistently scrolling quickly, without spurious spikes in acceleration
when we receive batched bluetooth events.

BUG=b:232137263
TEST=manual testing, check for degredation in unittests and
touchpad-tests, add touchpad-tests regression test.

Cq-Depend: chromium:5159508
Change-Id: I4b72fc0a2649dfc6628008668484e624f335a5e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/gestures/+/5159108
Reviewed-by: Kenneth Albanowski <kenalba@google.com>
Commit-Queue: Sean O'Brien <seobrien@chromium.org>
Reviewed-by: Aseda Aboagye <aaboagye@chromium.org>
Reviewed-by: Torsha Banerjee <torsha@google.com>
Tested-by: Sean O'Brien <seobrien@chromium.org>
Code-Coverage: Zoss <zoss-cl-coverage@prod.google.com>
diff --git a/include/mouse_interpreter.h b/include/mouse_interpreter.h
index e56be37..d1e12a3 100644
--- a/include/mouse_interpreter.h
+++ b/include/mouse_interpreter.h
@@ -45,9 +45,9 @@
   bool EmulateScrollWheel(const HardwareState& hwstate);
  private:
   struct WheelRecord {
-    WheelRecord(float v, stime_t t): value(v), timestamp(t) {}
-    WheelRecord(): value(0), timestamp(0) {}
-    float value;
+    WheelRecord(float v, stime_t t): change(v), timestamp(t) {}
+    WheelRecord(): change(0), timestamp(0) {}
+    float change;
     stime_t timestamp;
   };
 
@@ -60,8 +60,8 @@
 
   HardwareState prev_state_;
 
-  // Records last scroll wheel event.
-  WheelRecord last_wheel_, last_hwheel_;
+  // Records last scroll wheel events.
+  std::vector<WheelRecord> last_vertical_wheels_, last_horizontal_wheels_;
 
   // Accumulators to measure scroll distance while doing scroll wheel emulation
   double wheel_emulation_accu_x_;
@@ -85,6 +85,13 @@
   // Enable high-resolution scrolling.
   BoolProperty hi_res_scrolling_;
 
+  // When calculating scroll velocity for the purpose of acceleration, we
+  // use the average of this many events in the same direction. This is to avoid
+  // over-accelerating if we receive batched events with timestamps that are
+  // artificially close. If we don't have enough events, we won't accelerate at
+  // all.
+  IntProperty scroll_velocity_buffer_size_;
+
   // We use normal CDF to simulate scroll wheel acceleration curve. Use the
   // following method to generate the coefficients of a degree-4 polynomial
   // regression for a specific normal cdf in Python.
diff --git a/src/mouse_interpreter.cc b/src/mouse_interpreter.cc
index 6ed44e7..a281a9a 100644
--- a/src/mouse_interpreter.cc
+++ b/src/mouse_interpreter.cc
@@ -31,6 +31,7 @@
       scroll_sensitivity_(prop_reg,"Mouse Scroll Sensitivity",
         kMouseScrollSensitivityDefaultValue),
       hi_res_scrolling_(prop_reg, "Mouse High Resolution Scrolling", true),
+      scroll_velocity_buffer_size_(prop_reg, "Scroll Wheel Velocity Buffer", 3),
       scroll_accel_curve_prop_(prop_reg, "Mouse Scroll Accel Curve",
           scroll_accel_curve_, sizeof(scroll_accel_curve_) / sizeof(double)),
       scroll_max_allowed_input_speed_(prop_reg,
@@ -49,8 +50,6 @@
                                    "Output Mouse Wheel Gestures", false) {
   InitName();
   memset(&prev_state_, 0, sizeof(prev_state_));
-  memset(&last_wheel_, 0, sizeof(last_wheel_));
-  memset(&last_hwheel_, 0, sizeof(last_hwheel_));
   // Scroll acceleration curve coefficients. See the definition for more
   // details on how to generate them.
   scroll_accel_curve_[0] = 1.0374e+01;
@@ -168,60 +167,81 @@
                                                  bool is_vertical) {
   const char name[] = "MouseInterpreter::InterpretScrollWheelEvent";
 
-  const float scroll_wheel_event_time_delta_min = 0.008;
+  const size_t max_buffer_size = scroll_velocity_buffer_size_.val_;
+  const float scroll_wheel_event_time_delta_min = 0.008 * max_buffer_size;
   bool use_high_resolution =
       is_vertical && hwprops_->wheel_is_hi_res
       && hi_res_scrolling_.val_;
   // Vertical wheel or horizontal wheel.
-  float current_wheel_value = hwstate.rel_hwheel;
-  int ticks = hwstate.rel_hwheel * REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
-  WheelRecord* last_wheel_record = &last_hwheel_;
+  WheelRecord current_wheel;
+  current_wheel.timestamp = hwstate.timestamp;
+  int ticks;
+  std::vector<WheelRecord>* last_wheels;
   if (is_vertical) {
     // Only vertical high-res scrolling is supported for now.
     if (use_high_resolution) {
-      current_wheel_value = hwstate.rel_wheel_hi_res
+      current_wheel.change = hwstate.rel_wheel_hi_res
           / REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
       ticks = hwstate.rel_wheel_hi_res;
     } else {
-      current_wheel_value = hwstate.rel_wheel;
+      current_wheel.change = hwstate.rel_wheel;
       ticks = hwstate.rel_wheel * REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
     }
-    last_wheel_record = &last_wheel_;
+    last_wheels = &last_vertical_wheels_;
+  } else {
+    last_wheels = &last_horizontal_wheels_;
+    current_wheel.change = hwstate.rel_hwheel;
+    ticks = hwstate.rel_hwheel * REL_WHEEL_HI_RES_UNITS_PER_NOTCH;
   }
 
   // Check if the wheel is scrolled.
-  if (current_wheel_value) {
+  if (current_wheel.change) {
     stime_t start_time, end_time = hwstate.timestamp;
     // Check if this scroll is in same direction as previous scroll event.
-    if ((current_wheel_value < 0 && last_wheel_record->value < 0) ||
-        (current_wheel_value > 0 && last_wheel_record->value > 0)) {
-      start_time = last_wheel_record->timestamp;
+    if (!last_wheels->empty() &&
+        ((current_wheel.change < 0 && last_wheels->back().change < 0) ||
+         (current_wheel.change > 0 && last_wheels->back().change > 0))) {
+      start_time = last_wheels->begin()->timestamp;
     } else {
+      last_wheels->clear();
       start_time = end_time;
     }
 
-    // If start_time == end_time, compute velocity using dt = 1 second.
-    // (this happens when the user initially starts scrolling)
-    stime_t dt = (end_time - start_time) ?: 1.0;
-    if (dt < scroll_wheel_event_time_delta_min) {
-      // the first packet received after BT wakeup may be delayed, causing the
-      // time delta between that and the subsequent packet to be very small.
-      // Prevent small time deltas from triggering large amounts of acceleration
-      // by enforcing a minimum time delta.
-      dt = scroll_wheel_event_time_delta_min;
+    // We will only accelerate scrolls if we have filled our buffer of scroll
+    // events all in the same direction. If the buffer is full, then calculate
+    // scroll velocity using the average velocity of the entire buffer.
+    float velocity;
+    if (last_wheels->size() < max_buffer_size) {
+      velocity = 0.0;
+    } else {
+      stime_t dt = end_time - last_wheels->back().timestamp;
+      if (dt < scroll_wheel_event_time_delta_min) {
+        // The first packets received after BT wakeup may be delayed, causing
+        // the time delta between that and the subsequent packets to be
+        // artificially very small.
+        // Prevent small time deltas from triggering large amounts of
+        // acceleration by enforcing a minimum time delta.
+        dt = scroll_wheel_event_time_delta_min;
+      }
+
+      last_wheels->pop_back();
+      float buffer_scroll_distance = current_wheel.change;
+      for (auto wheel : *last_wheels) {
+        buffer_scroll_distance += wheel.change;
+      }
+
+      velocity = buffer_scroll_distance / dt;
     }
+    last_wheels->insert(last_wheels->begin(), current_wheel);
 
     // When scroll acceleration is off, the scroll factor does not relate to
     // scroll velocity. It's simply a constant multiplier to the wheel value.
     const double unaccel_scroll_factors[] = { 20.0, 36.0, 72.0, 112.0, 164.0 };
 
-    float velocity = current_wheel_value / dt;
-    float offset = current_wheel_value * (
+    float offset = current_wheel.change * (
       scroll_acceleration_.val_?
       ComputeScrollAccelFactor(velocity) :
       unaccel_scroll_factors[scroll_sensitivity_.val_ - 1]);
-    last_wheel_record->timestamp = hwstate.timestamp;
-    last_wheel_record->value = current_wheel_value;
 
     if (is_vertical) {
       // For historical reasons the vertical wheel (REL_WHEEL) is inverted
diff --git a/src/mouse_interpreter_unittest.cc b/src/mouse_interpreter_unittest.cc
index 57267d8..520a455 100644
--- a/src/mouse_interpreter_unittest.cc
+++ b/src/mouse_interpreter_unittest.cc
@@ -103,6 +103,7 @@
 
   mi.output_mouse_wheel_gestures_.val_ = true;
   mi.hi_res_scrolling_.val_ = 1;
+  mi.scroll_velocity_buffer_size_.val_ = 1;
 
   gs = wrapper.SyncInterpret(hwstates[0], nullptr);
   EXPECT_EQ(nullptr, gs);
@@ -157,6 +158,7 @@
   mi.scroll_acceleration_.val_ = true;
   mi.output_mouse_wheel_gestures_.val_ = true;
   mi.hi_res_scrolling_.val_ = false;
+  mi.scroll_velocity_buffer_size_.val_ = 1;
 
   gs = wrapper.SyncInterpret(hwstates[0], nullptr);
   EXPECT_EQ(nullptr, gs);
@@ -212,6 +214,7 @@
   };
 
   mi.output_mouse_wheel_gestures_.val_ = true;
+  mi.scroll_velocity_buffer_size_.val_ = 1;
 
   gs = wrapper.SyncInterpret(hwstates[0], nullptr);
   ASSERT_NE(nullptr, gs);
diff --git a/tools/touchtests-report.json b/tools/touchtests-report.json
index 8340e7b..145f6aa 100644
--- a/tools/touchtests-report.json
+++ b/tools/touchtests-report.json
@@ -1798,6 +1798,13 @@
     "result": "success",
     "score": 1.0
   },
+  "logitech-m650-1.0/fast_initial_scroll": {
+    "description": "",
+    "disabled": false,
+    "error": "",
+    "result": "success",
+    "score": 1.0
+  },
   "logitech-t620/accidental_back_fling": {
     "description": "",
     "disabled": false,