Bug 970751 Resample touch events. r=mwu,kats. a=kwierso
authorMason Chang <mchang@mozilla.com>
Sat, 30 Aug 2014 14:04:15 -0700
changeset 224335 cd57ffe572939190fd198020806ff691dc3b4499
parent 224334 42af440a510cbcf5bbcd971a7bb33a5b5929d9e6
child 224336 c12e29cfcc4340058b43935db879325ae7463d1e
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmwu, kats, kwierso
bugs970751
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 970751 Resample touch events. r=mwu,kats. a=kwierso
gfx/thebes/gfxPrefs.h
widget/gonk/GeckoTouchDispatcher.cpp
widget/gonk/GeckoTouchDispatcher.h
widget/gonk/HwcComposer2D.cpp
widget/gonk/moz.build
widget/gonk/nsAppShell.cpp
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -191,16 +191,21 @@ private:
   DECL_GFX_PREF(Live, "gfx.layerscope.port",                   LayerScopePort, int32_t, 23456);
   DECL_GFX_PREF(Live, "gfx.perf-warnings.enabled",             PerfWarnings, bool, false);
   DECL_GFX_PREF(Once, "gfx.work-around-driver-bugs",           WorkAroundDriverBugs, bool, true);
 
   DECL_GFX_PREF(Live, "gfx.draw-color-bars",                   CompositorDrawColorBars, bool, false);
 
   // Use vsync events generated by hardware
   DECL_GFX_PREF(Once, "gfx.frameuniformity.hw-vsync",          FrameUniformityHWVsyncEnabled, bool, false);
+  DECL_GFX_PREF(Once, "gfx.touch.resample",                    TouchResampling, bool, false);
+  // These times should be in nanoseconds
+  DECL_GFX_PREF(Once, "gfx.touch.resample.max-predict",        TouchResampleMaxPredict, int32_t, 8000000);
+  DECL_GFX_PREF(Once, "gfx.touch.resample.vsync-adjust",       TouchVsyncSampleAdjust, int32_t, 5000000);
+  DECL_GFX_PREF(Once, "gfx.touch.resample.min-resample",       TouchResampleMinTime, int32_t, 2000000);
 
   DECL_GFX_PREF(Live, "gl.msaa-level",                         MSAALevel, uint32_t, 2);
 
   DECL_GFX_PREF(Once, "layers.acceleration.disabled",          LayersAccelerationDisabled, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps",          LayersDrawFPS, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps.print-histogram",  FPSPrintHistogram, bool, false);
   DECL_GFX_PREF(Live, "layers.acceleration.draw-fps.write-to-file", WriteFPSToFile, bool, false);
   DECL_GFX_PREF(Once, "layers.acceleration.force-enabled",     LayersAccelerationForceEnabled, bool, false);
new file mode 100644
--- /dev/null
+++ b/widget/gonk/GeckoTouchDispatcher.cpp
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* Copyright 2014 Mozilla Foundation and Mozilla contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "FrameMetrics.h"
+#include "GeckoProfiler.h"
+#include "GeckoTouchDispatcher.h"
+#include "InputData.h"
+#include "base/basictypes.h"
+#include "gfxPrefs.h"
+#include "libui/Input.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/dom/Touch.h"
+#include "nsAppShell.h"
+#include "nsDebug.h"
+#include "nsThreadUtils.h"
+#include "nsWindow.h"
+#include <sys/types.h>
+#include <unistd.h>
+#include <utils/Timers.h>
+
+#define LOG(args...)                                            \
+  __android_log_print(ANDROID_LOG_INFO, "Gonk" , ## args)
+
+// uncomment to print log resample data
+// #define LOG_RESAMPLE_DATA 1
+
+namespace mozilla {
+
+// Amount of time in MS before an input is considered expired.
+static const uint64_t kInputExpirationThresholdMs = 1000;
+static int32_t nanosecToMillisec(int64_t nanosec) { return nanosec / 1000000; }
+
+static StaticRefPtr<GeckoTouchDispatcher> sTouchDispatcher;
+
+GeckoTouchDispatcher::GeckoTouchDispatcher()
+  : mTouchQueueLock("GeckoTouchDispatcher::mTouchQueueLock")
+  , mTouchEventsFiltered(false)
+  , mTouchDownCount(0)
+  , mTouchTimeDiff(0)
+  , mLastTouchTime(0)
+{
+  // Since GeckoTouchDispatcher is initialized when input is initialized
+  // and reads gfxPrefs, it is the first thing to touch gfxPrefs.
+  // The first thing to touch gfxPrefs MUST occur on the main thread and init
+  // the singleton
+  MOZ_ASSERT(sTouchDispatcher == nullptr);
+  MOZ_ASSERT(NS_IsMainThread());
+  gfxPrefs::GetSingleton();
+
+  mEnabledUniformityInfo = gfxPrefs::UniformityInfo();
+  mResamplingEnabled = gfxPrefs::TouchResampling() &&
+                       gfxPrefs::FrameUniformityHWVsyncEnabled();
+  mVsyncAdjust = gfxPrefs::TouchVsyncSampleAdjust();
+  mMaxPredict = gfxPrefs::TouchResampleMaxPredict();
+  mMinResampleTime = gfxPrefs::TouchResampleMinTime();
+  sTouchDispatcher = this;
+  ClearOnShutdown(&sTouchDispatcher);
+}
+
+class DispatchTouchEventsMainThread : public nsRunnable
+{
+public:
+  DispatchTouchEventsMainThread(GeckoTouchDispatcher* aTouchDispatcher,
+                                uint64_t aVsyncTime)
+    : mTouchDispatcher(aTouchDispatcher)
+    , mVsyncTime(aVsyncTime)
+  {
+  }
+
+  NS_IMETHOD Run()
+  {
+    mTouchDispatcher->DispatchTouchMoveEvents(mVsyncTime);
+    return NS_OK;
+  }
+
+private:
+  nsRefPtr<GeckoTouchDispatcher> mTouchDispatcher;
+  uint64_t mVsyncTime;
+};
+
+class DispatchSingleTouchMainThread : public nsRunnable
+{
+public:
+  DispatchSingleTouchMainThread(GeckoTouchDispatcher* aTouchDispatcher,
+                                MultiTouchInput& aTouch)
+    : mTouchDispatcher(aTouchDispatcher)
+    , mTouch(aTouch)
+  {
+  }
+
+  NS_IMETHOD Run()
+  {
+    mTouchDispatcher->DispatchTouchEvent(mTouch);
+    return NS_OK;
+  }
+
+private:
+  nsRefPtr<GeckoTouchDispatcher> mTouchDispatcher;
+  MultiTouchInput mTouch;
+};
+
+// Timestamp is in nanoseconds
+/* static */ bool
+GeckoTouchDispatcher::NotifyVsync(uint64_t aVsyncTimestamp)
+{
+  if (sTouchDispatcher == nullptr) {
+    return false;
+  }
+
+  MOZ_ASSERT(sTouchDispatcher->mResamplingEnabled);
+  bool haveTouchData = false;
+  {
+    MutexAutoLock lock(sTouchDispatcher->mTouchQueueLock);
+    haveTouchData = !sTouchDispatcher->mTouchMoveEvents.empty();
+  }
+
+  if (haveTouchData) {
+    NS_DispatchToMainThread(new DispatchTouchEventsMainThread(sTouchDispatcher, aVsyncTimestamp));
+  }
+
+  return haveTouchData;
+}
+
+// Touch data timestamps are in milliseconds, aEventTime is in nanoseconds
+void
+GeckoTouchDispatcher::NotifyTouch(MultiTouchInput& aData, uint64_t aEventTime)
+{
+  if (mResamplingEnabled) {
+    switch (aData.mType) {
+      case MultiTouchInput::MULTITOUCH_MOVE:
+      {
+        MutexAutoLock lock(mTouchQueueLock);
+        mTouchMoveEvents.push_back(aData);
+        mTouchTimeDiff = aEventTime - mLastTouchTime;
+        mLastTouchTime = aEventTime;
+        return;
+      }
+      default:
+        break;
+    }
+  }
+
+  NS_DispatchToMainThread(new DispatchSingleTouchMainThread(this, aData));
+}
+
+void
+GeckoTouchDispatcher::DispatchTouchMoveEvents(uint64_t aVsyncTime)
+{
+  MultiTouchInput touchMove;
+
+  {
+    MutexAutoLock lock(mTouchQueueLock);
+    if (mTouchMoveEvents.empty()) {
+      return;
+    }
+
+    int touchCount = mTouchMoveEvents.size();
+    // Both aVsynctime and mLastTouchTime are uint64_t
+    // Need to store as a signed int.
+    int64_t vsyncTouchDiff = aVsyncTime - mLastTouchTime;
+    bool resample = (touchCount > 1) &&
+                    (vsyncTouchDiff > mMinResampleTime);
+
+    if (!resample) {
+      touchMove = mTouchMoveEvents.back();
+      mTouchMoveEvents.clear();
+      mTouchMoveEvents.push_back(touchMove);
+    } else {
+      ResampleTouchMoves(touchMove, aVsyncTime);
+    }
+  }
+
+  DispatchTouchEvent(touchMove);
+}
+
+static int
+Interpolate(int start, int end, int64_t aFrameDiff, int64_t aTouchDiff)
+{
+  return start + (((end - start) * aFrameDiff) / aTouchDiff);
+}
+
+static const SingleTouchData&
+GetTouchByID(const SingleTouchData& aCurrentTouch, MultiTouchInput& aOtherTouch)
+{
+  int32_t id = aCurrentTouch.mIdentifier;
+  for (size_t i = 0; i < aOtherTouch.mTouches.Length(); i++) {
+    SingleTouchData& touch = aOtherTouch.mTouches[i];
+    if (touch.mIdentifier == id) {
+      return touch;
+    }
+  }
+
+  // We can have situations where a previous touch event had 2 fingers
+  // and we lift 1 finger off. In those cases, we won't find the touch event
+  // with given id, so just return the current touch, which will be resampled
+  // without modification and dispatched.
+  return aCurrentTouch;
+}
+
+static void
+ResampleTouch(MultiTouchInput& aOutTouch, MultiTouchInput& aCurrent,
+              MultiTouchInput& aOther, int64_t aFrameDiff,
+              int64_t aTouchDiff, bool aInterpolate)
+{
+  aOutTouch = aCurrent;
+
+  // Make sure we only resample the correct finger.
+  for (size_t i = 0; i < aOutTouch.mTouches.Length(); i++) {
+    const SingleTouchData& current = aCurrent.mTouches[i];
+    const SingleTouchData& other = GetTouchByID(current, aOther);
+
+    const ScreenIntPoint& currentTouchPoint = current.mScreenPoint;
+    const ScreenIntPoint& otherTouchPoint = other.mScreenPoint;
+
+    ScreenIntPoint newSamplePoint;
+    newSamplePoint.x = Interpolate(currentTouchPoint.x, otherTouchPoint.x, aFrameDiff, aTouchDiff);
+    newSamplePoint.y = Interpolate(currentTouchPoint.y, otherTouchPoint.y, aFrameDiff, aTouchDiff);
+
+    aOutTouch.mTouches[i].mScreenPoint = newSamplePoint;
+
+#ifdef LOG_RESAMPLE_DATA
+    const char* type = "extrapolate";
+    if (aInterpolate) {
+      type = "interpolate";
+    }
+
+    float alpha = (double) aFrameDiff / (double) aTouchDiff;
+    LOG("%s current (%d, %d), other (%d, %d) to (%d, %d) alpha %f, touch diff %llu, frame diff %lld\n",
+        type,
+        currentTouchPoint.x, currentTouchPoint.y,
+        otherTouchPoint.x, otherTouchPoint.y,
+        newSamplePoint.x, newSamplePoint.y,
+        alpha, aTouchDiff, aFrameDiff);
+#endif
+  }
+}
+
+// Interpolates with the touch event prior to SampleTime
+// and with the future touch event past sample time
+int32_t
+GeckoTouchDispatcher::InterpolateTouch(MultiTouchInput& aOutTouch, uint64_t aSampleTime)
+{
+  MOZ_RELEASE_ASSERT(mTouchMoveEvents.size() >= 2);
+  mTouchQueueLock.AssertCurrentThreadOwns();
+
+  // currentTouch < SampleTime < futureTouch
+  MultiTouchInput futureTouch = mTouchMoveEvents.back();
+  mTouchMoveEvents.pop_back();
+  MultiTouchInput currentTouch = mTouchMoveEvents.back();
+
+  mTouchMoveEvents.clear();
+  mTouchMoveEvents.push_back(futureTouch);
+
+  uint64_t currentTouchTime = mLastTouchTime - mTouchTimeDiff;
+  int64_t frameDiff = aSampleTime - currentTouchTime;
+  ResampleTouch(aOutTouch, currentTouch, futureTouch, frameDiff, mTouchTimeDiff, true);
+
+  return nanosecToMillisec(frameDiff);
+}
+
+// Extrapolates from the previous two touch events before sample time
+// and extrapolates them to sample time.
+int32_t
+GeckoTouchDispatcher::ExtrapolateTouch(MultiTouchInput& aOutTouch, uint64_t aSampleTime)
+{
+  MOZ_RELEASE_ASSERT(mTouchMoveEvents.size() >= 2);
+  mTouchQueueLock.AssertCurrentThreadOwns();
+
+  // prevTouch < currentTouch < SampleTime
+  MultiTouchInput currentTouch = mTouchMoveEvents.back();
+  mTouchMoveEvents.pop_back();
+  MultiTouchInput prevTouch = mTouchMoveEvents.back();
+  mTouchMoveEvents.clear();
+  mTouchMoveEvents.push_back(currentTouch);
+
+  uint64_t currentTouchTime = mLastTouchTime;
+  int64_t maxResampleTime = std::min(mTouchTimeDiff / 2, (int64_t) mMaxPredict);
+  uint64_t maxTimestamp = currentTouchTime + maxResampleTime;
+
+  if (aSampleTime > maxTimestamp) {
+    aSampleTime = maxTimestamp;
+    #ifdef LOG_RESAMPLE_DATA
+    LOG("Overshot extrapolation time, adjusting sample time\n");
+    #endif
+  }
+
+  // This has to be signed int since it is negative
+  int64_t frameDiff = currentTouchTime - aSampleTime;
+  ResampleTouch(aOutTouch, currentTouch, prevTouch, frameDiff, mTouchTimeDiff, false);
+  return -nanosecToMillisec(frameDiff);
+}
+
+void
+GeckoTouchDispatcher::ResampleTouchMoves(MultiTouchInput& aOutTouch, uint64_t aVsyncTime)
+{
+  uint64_t sampleTime = aVsyncTime - mVsyncAdjust;
+  int32_t touchTimeAdjust = 0;
+
+  if (mLastTouchTime > sampleTime) {
+    touchTimeAdjust = InterpolateTouch(aOutTouch, sampleTime);
+  } else {
+    touchTimeAdjust = ExtrapolateTouch(aOutTouch, sampleTime);
+  }
+
+  aOutTouch.mTimeStamp += TimeDuration::FromMilliseconds(touchTimeAdjust);
+  aOutTouch.mTime += touchTimeAdjust;
+}
+
+// Some touch events get sent as mouse events. If APZ doesn't capture the event
+// and if a touch only has 1 touch input, we can send a mouse event.
+void
+GeckoTouchDispatcher::DispatchMouseEvent(MultiTouchInput& aMultiTouch,
+                                         bool aForwardToChildren)
+{
+  WidgetMouseEvent mouseEvent = ToWidgetMouseEvent(aMultiTouch, nullptr);
+  if (mouseEvent.message == NS_EVENT_NULL) {
+    return;
+  }
+
+  mouseEvent.mFlags.mNoCrossProcessBoundaryForwarding = !aForwardToChildren;
+  nsWindow::DispatchInputEvent(mouseEvent);
+}
+
+static bool
+IsExpired(const MultiTouchInput& aTouch)
+{
+  // No pending events, the filter state can be updated.
+  uint64_t timeNowMs = systemTime(SYSTEM_TIME_MONOTONIC) / 1000000;
+  return (timeNowMs - aTouch.mTime) > kInputExpirationThresholdMs;
+}
+void
+GeckoTouchDispatcher::DispatchTouchEvent(MultiTouchInput& aMultiTouch)
+{
+  if (!mTouchDownCount) {
+    mTouchEventsFiltered = IsExpired(aMultiTouch);
+  }
+
+  switch (aMultiTouch.mType) {
+    case MultiTouchInput::MULTITOUCH_START:
+      mTouchDownCount++;
+      break;
+    case MultiTouchInput::MULTITOUCH_MOVE:
+      break;
+    case MultiTouchInput::MULTITOUCH_END:
+    case MultiTouchInput::MULTITOUCH_CANCEL:
+      mTouchDownCount--;
+      if (mTouchDownCount == 0) {
+        MutexAutoLock lock(mTouchQueueLock);
+        mTouchMoveEvents.clear();
+      }
+      break;
+    default:
+      break;
+  }
+
+  if (mTouchEventsFiltered) {
+    return;
+  }
+
+  bool captured = false;
+  WidgetTouchEvent event = aMultiTouch.ToWidgetTouchEvent(nullptr);
+  nsEventStatus status = nsWindow::DispatchInputEvent(event, &captured);
+
+  if (mEnabledUniformityInfo) {
+    const char* touchAction = "Invalid";
+    switch (aMultiTouch.mType) {
+      case MultiTouchInput::MULTITOUCH_START:
+        touchAction = "Touch_Event_Down";
+        break;
+      case MultiTouchInput::MULTITOUCH_MOVE:
+        touchAction = "Touch_Event_Move";
+        break;
+      case MultiTouchInput::MULTITOUCH_END:
+      case MultiTouchInput::MULTITOUCH_CANCEL:
+        touchAction = "Touch_Event_Up";
+        break;
+    }
+
+    const SingleTouchData& firstTouch = aMultiTouch.mTouches[0];
+    const ScreenIntPoint& touchPoint = firstTouch.mScreenPoint;
+
+    LOG("UniformityInfo %s %llu %d %d", touchAction, systemTime(SYSTEM_TIME_MONOTONIC),
+        touchPoint.x, touchPoint.y);
+  }
+
+  if (!captured && (aMultiTouch.mTouches.Length() == 1)) {
+    bool forwardToChildren = status != nsEventStatus_eConsumeNoDefault;
+    DispatchMouseEvent(aMultiTouch, forwardToChildren);
+  }
+}
+
+WidgetMouseEvent
+GeckoTouchDispatcher::ToWidgetMouseEvent(const MultiTouchInput& aMultiTouch,
+                                         nsIWidget* aWidget) const
+{
+  NS_ABORT_IF_FALSE(NS_IsMainThread(),
+                    "Can only convert To WidgetMouseEvent on main thread");
+
+  uint32_t mouseEventType = NS_EVENT_NULL;
+  switch (aMultiTouch.mType) {
+    case MultiTouchInput::MULTITOUCH_START:
+      mouseEventType = NS_MOUSE_BUTTON_DOWN;
+      break;
+    case MultiTouchInput::MULTITOUCH_MOVE:
+      mouseEventType = NS_MOUSE_MOVE;
+      break;
+    case MultiTouchInput::MULTITOUCH_CANCEL:
+    case MultiTouchInput::MULTITOUCH_END:
+      mouseEventType = NS_MOUSE_BUTTON_UP;
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Did not assign a type to WidgetMouseEvent");
+      break;
+  }
+
+  WidgetMouseEvent event(true, mouseEventType, aWidget,
+                         WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
+
+  const SingleTouchData& firstTouch = aMultiTouch.mTouches[0];
+  event.refPoint.x = firstTouch.mScreenPoint.x;
+  event.refPoint.y = firstTouch.mScreenPoint.y;
+
+  event.time = aMultiTouch.mTime;
+  event.button = WidgetMouseEvent::eLeftButton;
+  event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
+  event.modifiers = aMultiTouch.modifiers;
+
+  if (mouseEventType != NS_MOUSE_MOVE) {
+    event.clickCount = 1;
+  }
+
+  return event;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/widget/gonk/GeckoTouchDispatcher.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* Copyright 2014 Mozilla Foundation and Mozilla contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef GECKO_TOUCH_INPUT_DISPATCHER_h
+#define GECKO_TOUCH_INPUT_DISPATCHER_h
+
+#include "InputData.h"
+#include "Units.h"
+#include "mozilla/Mutex.h"
+#include <vector>
+
+class nsIWidget;
+
+namespace mozilla {
+class WidgetMouseEvent;
+
+// Used to resample touch events whenever a vsync event occurs. It batches
+// touch moves and on every vsync, resamples the touch position to create smooth
+// scrolls. We use the Android touch resample algorithm. It uses a combination of
+// extrapolation and interpolation. The algorithm takes the vsync time and
+// subtracts mVsyncAdjust time in ms and creates a sample time. All touch events are
+// relative to this sample time. If the last touch event occurs AFTER this
+// sample time, interpolate the last two touch events. If the last touch event occurs BEFORE
+// this sample time, we extrapolate the last two touch events to the sample
+// time. The magic numbers defined as constants are taken from android
+// InputTransport.cpp.
+class GeckoTouchDispatcher
+{
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GeckoTouchDispatcher)
+
+public:
+  GeckoTouchDispatcher();
+  void NotifyTouch(MultiTouchInput& aData, uint64_t aEventTime);
+  void DispatchTouchEvent(MultiTouchInput& aMultiTouch);
+  void DispatchTouchMoveEvents(uint64_t aVsyncTime);
+  static bool NotifyVsync(uint64_t aVsyncTimestamp);
+
+private:
+  int32_t InterpolateTouch(MultiTouchInput& aOutTouch, uint64_t aSampleTime);
+  int32_t ExtrapolateTouch(MultiTouchInput& aOutTouch, uint64_t aSampleTime);
+  void ResampleTouchMoves(MultiTouchInput& aOutTouch, uint64_t vsyncTime);
+  void SendTouchEvent(MultiTouchInput& aData);
+  void DispatchMouseEvent(MultiTouchInput& aMultiTouch,
+                          bool aForwardToChildren);
+  WidgetMouseEvent ToWidgetMouseEvent(const MultiTouchInput& aData, nsIWidget* aWidget) const;
+
+  // mTouchQueueLock are used to protect the vector below
+  // as it is accessed on the vsync thread and main thread
+  Mutex mTouchQueueLock;
+  std::vector<MultiTouchInput> mTouchMoveEvents;
+
+  bool mResamplingEnabled;
+  bool mTouchEventsFiltered;
+  bool mEnabledUniformityInfo;
+  int mTouchDownCount;
+
+  // All times below are in nanoseconds
+  int32_t mVsyncAdjust;     // Time from vsync we create sample times from
+  int32_t mMaxPredict;      // How far into the future we're allowed to extrapolate
+
+  // Amount of time between vsync and the last event that is required before we
+  // resample
+  int32_t mMinResampleTime;
+
+  // The time difference between the last two touch move events
+  int64_t mTouchTimeDiff;
+
+  // The system time at which the last touch event occured
+  uint64_t mLastTouchTime;
+};
+
+} // namespace mozilla
+#endif // GECKO_TOUCH_INPUT_DISPATCHER_h
--- a/widget/gonk/HwcComposer2D.cpp
+++ b/widget/gonk/HwcComposer2D.cpp
@@ -23,16 +23,17 @@
 #include "LayerScope.h"
 #include "mozilla/layers/LayerManagerComposite.h"
 #include "mozilla/layers/PLayerTransaction.h"
 #include "mozilla/layers/ShadowLayerUtilsGralloc.h"
 #include "mozilla/layers/TextureHostOGL.h"  // for TextureHostOGL
 #include "mozilla/StaticPtr.h"
 #include "cutils/properties.h"
 #include "gfx2DGlue.h"
+#include "GeckoTouchDispatcher.h"
 
 #if ANDROID_VERSION >= 17
 #include "libdisplay/FramebufferSurface.h"
 #include "gfxPrefs.h"
 #include "nsThreadUtils.h"
 
 #ifndef HWC_BLIT
 #define HWC_BLIT (HWC_FRAMEBUFFER_TARGET + 1)
@@ -144,16 +145,20 @@ HwcComposer2D::Init(hwc_display_t dpy, h
         }
         if (mHwc->query(mHwc, HwcUtils::HWC_FORMAT_RB_SWAP, &supported) == NO_ERROR) {
             mRBSwapSupport = !!supported;
         }
     } else {
         mColorFill = false;
         mRBSwapSupport = false;
     }
+
+    if (RegisterHwcEventCallback()) {
+        EnableVsync(true);
+    }
 #else
     char propValue[PROPERTY_VALUE_MAX];
     property_get("ro.display.colorfill", propValue, "0");
     mColorFill = (atoi(propValue) == 1) ? true : false;
     mRBSwapSupport = true;
 #endif
 
     mDpy = dpy;
@@ -218,17 +223,17 @@ HwcComposer2D::RunVsyncEventControl(bool
             device->eventControl(device, HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, aEnable);
         }
     }
 }
 
 void
 HwcComposer2D::Vsync(int aDisplay, int64_t aTimestamp)
 {
-    // TODO: Handle Vsync event here
+    GeckoTouchDispatcher::NotifyVsync(aTimestamp);
 }
 #endif
 
 bool
 HwcComposer2D::ReallocLayerList()
 {
     int size = sizeof(HwcList) +
         ((mMaxLayerCount + LAYER_COUNT_INCREMENTS) * sizeof(HwcLayer));
--- a/widget/gonk/moz.build
+++ b/widget/gonk/moz.build
@@ -39,16 +39,17 @@ SOURCES += ['libui/' + src for src in [
     'SpriteController.cpp',
     'Tokenizer.cpp',
     'VelocityControl.cpp',
     'VelocityTracker.cpp',
     'VirtualKeyMap.cpp',
 ]]
 
 SOURCES += [
+    'GeckoTouchDispatcher.cpp',
     'GfxInfo.cpp',
     'GonkMemoryPressureMonitoring.cpp',
     'GonkPermission.cpp',
     'HwcComposer2D.cpp',
     'HwcUtils.cpp',
     'nsAppShell.cpp',
     'nsClipboard.cpp',
     'nsIdleServiceGonk.cpp',
--- a/widget/gonk/nsAppShell.cpp
+++ b/widget/gonk/nsAppShell.cpp
@@ -67,16 +67,17 @@
 #include "ipc/Nuwa.h"
 #endif
 
 #include "mozilla/Preferences.h"
 #include "GeckoProfiler.h"
 
 // Defines kKeyMapping and GetKeyNameIndex()
 #include "GonkKeyMapping.h"
+#include "GeckoTouchDispatcher.h"
 
 #define LOG(args...)                                            \
     __android_log_print(ANDROID_LOG_INFO, "Gonk" , ## args)
 #ifdef VERBOSE_LOG_ENABLED
 # define VERBOSE_LOG(args...)                           \
     __android_log_print(ANDROID_LOG_INFO, "Gonk" , ## args)
 #else
 # define VERBOSE_LOG(args...)                   \
@@ -152,24 +153,22 @@ struct UserInputData {
             int32_t keyCode;
             int32_t scanCode;
         } key;
         struct {
             int32_t touchCount;
             ::Touch touches[MAX_POINTERS];
         } motion;
     };
-
-    Modifiers DOMModifiers() const;
 };
 
-Modifiers
-UserInputData::DOMModifiers() const
+static mozilla::Modifiers
+getDOMModifiers(int32_t metaState)
 {
-    Modifiers result = 0;
+    mozilla::Modifiers result = 0;
     if (metaState & (AMETA_ALT_ON | AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
         result |= MODIFIER_ALT;
     }
     if (metaState & (AMETA_SHIFT_ON |
                      AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
         result |= MODIFIER_SHIFT;
     }
     if (metaState & AMETA_FUNCTION_ON) {
@@ -190,116 +189,16 @@ UserInputData::DOMModifiers() const
         result |= MODIFIER_NUMLOCK;
     }
     if (metaState & AMETA_SCROLL_LOCK_ON) {
         result |= MODIFIER_SCROLLLOCK;
     }
     return result;
 }
 
-static void
-sendMouseEvent(uint32_t msg, UserInputData& data, bool forwardToChildren)
-{
-    WidgetMouseEvent event(true, msg, nullptr,
-                           WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
-
-    event.refPoint.x = data.motion.touches[0].coords.getX();
-    event.refPoint.y = data.motion.touches[0].coords.getY();
-    event.time = data.timeMs;
-    event.button = WidgetMouseEvent::eLeftButton;
-    event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
-    if (msg != NS_MOUSE_MOVE)
-        event.clickCount = 1;
-    event.modifiers = data.DOMModifiers();
-
-    event.mFlags.mNoCrossProcessBoundaryForwarding = !forwardToChildren;
-
-    nsWindow::DispatchInputEvent(event);
-}
-
-static void
-addDOMTouch(UserInputData& data, WidgetTouchEvent& event, int i)
-{
-    const ::Touch& touch = data.motion.touches[i];
-    event.touches.AppendElement(
-        new dom::Touch(touch.id,
-                       nsIntPoint(floor(touch.coords.getX() + 0.5), floor(touch.coords.getY() + 0.5)),
-                       nsIntPoint(touch.coords.getAxisValue(AMOTION_EVENT_AXIS_SIZE),
-                                  touch.coords.getAxisValue(AMOTION_EVENT_AXIS_SIZE)),
-                       0,
-                       touch.coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE))
-    );
-}
-
-static void
-printUniformityInfo(UserInputData& aData)
-{
-    char* touchAction;
-    const ::Touch& touch = aData.motion.touches[0];
-    int32_t action = aData.action & AMOTION_EVENT_ACTION_MASK;
-    switch (action) {
-    case AMOTION_EVENT_ACTION_DOWN:
-         touchAction = "Touch_Event_Down";
-         break;
-    case AMOTION_EVENT_ACTION_MOVE:
-         touchAction = "Touch_Event_Move";
-          break;
-    case AMOTION_EVENT_ACTION_UP:
-         touchAction = "Touch_Event_Up";
-         break;
-    default :
-         return;
-    }
-    LOG("UniformityInfo %s %llu %f %f", touchAction, systemTime(SYSTEM_TIME_MONOTONIC),
-        touch.coords.getX(),  touch.coords.getY() );
-}
-
-static nsEventStatus
-sendTouchEvent(UserInputData& data, bool* captured)
-{
-    uint32_t msg;
-    int32_t action = data.action & AMOTION_EVENT_ACTION_MASK;
-    switch (action) {
-    case AMOTION_EVENT_ACTION_DOWN:
-    case AMOTION_EVENT_ACTION_POINTER_DOWN:
-        msg = NS_TOUCH_START;
-        break;
-    case AMOTION_EVENT_ACTION_MOVE:
-        msg = NS_TOUCH_MOVE;
-        break;
-    case AMOTION_EVENT_ACTION_UP:
-    case AMOTION_EVENT_ACTION_POINTER_UP:
-        msg = NS_TOUCH_END;
-        break;
-    case AMOTION_EVENT_ACTION_OUTSIDE:
-    case AMOTION_EVENT_ACTION_CANCEL:
-        msg = NS_TOUCH_CANCEL;
-        break;
-    default:
-        return nsEventStatus_eIgnore;
-    }
-
-    WidgetTouchEvent event(true, msg, nullptr);
-
-    event.time = data.timeMs;
-    event.modifiers = data.DOMModifiers();
-
-    int32_t i;
-    if (msg == NS_TOUCH_END) {
-        i = data.action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK;
-        i >>= AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
-        addDOMTouch(data, event, i);
-    } else {
-        for (i = 0; i < data.motion.touchCount; ++i)
-            addDOMTouch(data, event, i);
-    }
-
-    return nsWindow::DispatchInputEvent(event, captured);
-}
-
 class MOZ_STACK_CLASS KeyEventDispatcher
 {
 public:
     KeyEventDispatcher(const UserInputData& aData,
                        KeyCharacterMap* aKeyCharMap);
     void Dispatch();
 
 private:
@@ -400,17 +299,17 @@ KeyEventDispatcher::DispatchKeyEventInte
     }
     event.isChar = !!event.charCode;
     event.mIsRepeat = IsRepeat();
     event.mKeyNameIndex = mDOMKeyNameIndex;
     if (mDOMPrintableKeyValue) {
         event.mKeyValue = mDOMPrintableKeyValue;
     }
     event.mCodeNameIndex = mDOMCodeNameIndex;
-    event.modifiers = mData.DOMModifiers();
+    event.modifiers = getDOMModifiers(mData.metaState);
     event.location = nsIDOMKeyEvent::DOM_KEY_LOCATION_MOBILE;
     event.time = mData.timeMs;
     return nsWindow::DispatchInputEvent(event);
 }
 
 void
 KeyEventDispatcher::Dispatch()
 {
@@ -589,22 +488,20 @@ protected:
     virtual ~GeckoInputReaderPolicy() {}
 };
 
 class GeckoInputDispatcher : public InputDispatcherInterface {
 public:
     GeckoInputDispatcher(sp<EventHub> &aEventHub)
         : mQueueLock("GeckoInputDispatcher::mQueueMutex")
         , mEventHub(aEventHub)
-        , mTouchDownCount(0)
         , mKeyDownCount(0)
-        , mTouchEventsFiltered(false)
         , mKeyEventsFiltered(false)
     {
-      mEnabledUniformityInfo = Preferences::GetBool("layers.uniformity-info", false);
+        mTouchDispatcher = new GeckoTouchDispatcher();
     }
 
     virtual void dump(String8& dump);
 
     virtual void monitor() {}
 
     // Called on the main thread
     virtual void dispatchOnce();
@@ -630,32 +527,29 @@ public:
 
     virtual status_t registerInputChannel(const sp<InputChannel>& inputChannel,
             const sp<InputWindowHandle>& inputWindowHandle, bool monitor);
     virtual status_t unregisterInputChannel(const sp<InputChannel>& inputChannel);
 
 
 
 protected:
-    virtual ~GeckoInputDispatcher() {}
+    virtual ~GeckoInputDispatcher() { }
 
 private:
     // mQueueLock should generally be locked while using mEventQueue.
     // UserInputData is pushed on on the InputReaderThread and
     // popped and dispatched on the main thread.
     mozilla::Mutex mQueueLock;
     std::queue<UserInputData> mEventQueue;
     sp<EventHub> mEventHub;
+    nsRefPtr<GeckoTouchDispatcher> mTouchDispatcher;
 
-    int mTouchDownCount;
     int mKeyDownCount;
-    bool mTouchEventsFiltered;
     bool mKeyEventsFiltered;
-    BitSet32 mTouchDown;
-    bool mEnabledUniformityInfo;
 };
 
 // GeckoInputReaderPolicy
 void
 GeckoInputReaderPolicy::setDisplayInfo()
 {
     static_assert(nsIScreen::ROTATION_0_DEG ==
                   DISPLAY_ORIENTATION_0,
@@ -717,90 +611,17 @@ GeckoInputDispatcher::dispatchOnce()
         data = mEventQueue.front();
         mEventQueue.pop();
         if (!mEventQueue.empty())
             gAppShell->NotifyNativeEvent();
     }
 
     switch (data.type) {
     case UserInputData::MOTION_DATA: {
-        if (!mTouchDownCount) {
-            // No pending events, the filter state can be updated.
-            mTouchEventsFiltered = isExpired(data);
-        }
-
-        int32_t action = data.action & AMOTION_EVENT_ACTION_MASK;
-        int32_t index = data.action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK;
-        index >>= AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
-        int32_t id = data.motion.touches[index].id;
-        switch (action) {
-        case AMOTION_EVENT_ACTION_DOWN:
-        case AMOTION_EVENT_ACTION_POINTER_DOWN:
-            if (!mTouchDown.hasBit(id)) {
-                mTouchDown.markBit(id);
-                mTouchDownCount++;
-            }
-            break;
-        case AMOTION_EVENT_ACTION_MOVE:
-        case AMOTION_EVENT_ACTION_HOVER_MOVE:
-            // No need to update the count on move.
-            break;
-        case AMOTION_EVENT_ACTION_UP:
-        case AMOTION_EVENT_ACTION_POINTER_UP:
-        case AMOTION_EVENT_ACTION_OUTSIDE:
-        case AMOTION_EVENT_ACTION_CANCEL:
-            if (mTouchDown.hasBit(id)) {
-                mTouchDown.clearBit(id);
-                mTouchDownCount--;
-            }
-            break;
-        default:
-            break;
-        }
-
-        if (mTouchEventsFiltered) {
-            return;
-        }
-
-        nsEventStatus status = nsEventStatus_eIgnore;
-        if (action != AMOTION_EVENT_ACTION_HOVER_MOVE) {
-            bool captured;
-            status = sendTouchEvent(data, &captured);
-            if (mEnabledUniformityInfo) {
-                printUniformityInfo(data);
-            }
-            if (captured) {
-                return;
-            }
-        }
-
-        uint32_t msg;
-        switch (action) {
-        case AMOTION_EVENT_ACTION_DOWN:
-            msg = NS_MOUSE_BUTTON_DOWN;
-            break;
-        case AMOTION_EVENT_ACTION_POINTER_DOWN:
-        case AMOTION_EVENT_ACTION_POINTER_UP:
-        case AMOTION_EVENT_ACTION_MOVE:
-        case AMOTION_EVENT_ACTION_HOVER_MOVE:
-            msg = NS_MOUSE_MOVE;
-            break;
-        case AMOTION_EVENT_ACTION_OUTSIDE:
-        case AMOTION_EVENT_ACTION_CANCEL:
-        case AMOTION_EVENT_ACTION_UP:
-            msg = NS_MOUSE_BUTTON_UP;
-            break;
-        default:
-            msg = NS_EVENT_NULL;
-            break;
-        }
-        if (msg != NS_EVENT_NULL) {
-            sendMouseEvent(msg, data, 
-                           status != nsEventStatus_eConsumeNoDefault);
-        }
+        MOZ_ASSERT_UNREACHABLE("Should not dispatch touch events here anymore");
         break;
     }
     case UserInputData::KEY_DATA: {
         if (!mKeyDownCount) {
             // No pending events, the filter state can be updated.
             mKeyEventsFiltered = isExpired(data);
         }
 
@@ -836,51 +657,82 @@ GeckoInputDispatcher::notifyKey(const No
     data.key.scanCode = args->scanCode;
     {
         MutexAutoLock lock(mQueueLock);
         mEventQueue.push(data);
     }
     gAppShell->NotifyNativeEvent();
 }
 
+static void
+addMultiTouch(MultiTouchInput& aMultiTouch,
+                                    const NotifyMotionArgs* args, int aIndex)
+{
+    int32_t id = args->pointerProperties[aIndex].id;
+    PointerCoords coords = args->pointerCoords[aIndex];
+    float force = coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
+    ScreenIntPoint point(floor(coords.getX() + 0.5),
+                         floor(coords.getY() + 0.5));
+
+    SingleTouchData touchData(id, point, ScreenSize(0, 0),
+                              0, force);
+
+    aMultiTouch.mTouches.AppendElement(touchData);
+}
 
 void
 GeckoInputDispatcher::notifyMotion(const NotifyMotionArgs* args)
 {
-    UserInputData data;
-    data.timeMs = nanosecsToMillisecs(args->eventTime);
-    data.type = UserInputData::MOTION_DATA;
-    data.action = args->action;
-    data.flags = args->flags;
-    data.metaState = args->metaState;
-    data.deviceId = args->deviceId;
-    MOZ_ASSERT(args->pointerCount <= MAX_POINTERS);
-    data.motion.touchCount = args->pointerCount;
-    for (uint32_t i = 0; i < args->pointerCount; ++i) {
-        ::Touch& touch = data.motion.touches[i];
-        touch.id = args->pointerProperties[i].id;
-        memcpy(&touch.coords, &args->pointerCoords[i], sizeof(*args->pointerCoords));
+    uint32_t time = nanosecsToMillisecs(args->eventTime);
+    int32_t action = args->action & AMOTION_EVENT_ACTION_MASK;
+    int touchCount = args->pointerCount;
+    MOZ_ASSERT(touchCount <= MAX_POINTERS);
+    TimeStamp timestamp = TimeStamp::Now();
+    Modifiers modifiers = getDOMModifiers(args->metaState);
+
+    MultiTouchInput::MultiTouchType touchType = MultiTouchInput::MULTITOUCH_CANCEL;
+    switch (action) {
+    case AMOTION_EVENT_ACTION_DOWN:
+    case AMOTION_EVENT_ACTION_POINTER_DOWN:
+        touchType = MultiTouchInput::MULTITOUCH_START;
+        break;
+    case AMOTION_EVENT_ACTION_MOVE:
+        touchType = MultiTouchInput::MULTITOUCH_MOVE;
+        break;
+    case AMOTION_EVENT_ACTION_UP:
+    case AMOTION_EVENT_ACTION_POINTER_UP:
+        touchType = MultiTouchInput::MULTITOUCH_END;
+        break;
+    case AMOTION_EVENT_ACTION_OUTSIDE:
+    case AMOTION_EVENT_ACTION_CANCEL:
+        touchType = MultiTouchInput::MULTITOUCH_CANCEL;
+        break;
+    default:
+        MOZ_ASSERT_UNREACHABLE("Could not assign a touch type");
+        break;
     }
-    {
-        MutexAutoLock lock(mQueueLock);
-        if (!mEventQueue.empty() &&
-             mEventQueue.back().type == UserInputData::MOTION_DATA &&
-           ((mEventQueue.back().action & AMOTION_EVENT_ACTION_MASK) ==
-             AMOTION_EVENT_ACTION_MOVE ||
-            (mEventQueue.back().action & AMOTION_EVENT_ACTION_MASK) ==
-             AMOTION_EVENT_ACTION_HOVER_MOVE))
-            mEventQueue.back() = data;
-        else
-            mEventQueue.push(data);
+
+    MultiTouchInput touchData(touchType, time, timestamp, modifiers);
+
+    // For touch ends, we have to filter out which finger is actually
+    // the touch end since the touch array has all fingers, not just the touch
+    // that we want to end
+    if (touchType == MultiTouchInput::MULTITOUCH_END) {
+        int touchIndex = args->action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK;
+        touchIndex >>= AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+        addMultiTouch(touchData, args, touchIndex);
+    } else {
+        for (int32_t i = 0; i < touchCount; ++i) {
+            addMultiTouch(touchData, args, i);
+        }
     }
-    gAppShell->NotifyNativeEvent();
+
+    mTouchDispatcher->NotifyTouch(touchData, args->eventTime);
 }
 
-
-
 void GeckoInputDispatcher::notifySwitch(const NotifySwitchArgs* args)
 {
     if (!sDevInputAudioJack)
         return;
 
     bool needSwitchUpdate = false;
 
     if (args->switchMask & (1 << SW_HEADPHONE_INSERT)) {