Bug 856427: Add vsync support at the refresh driver (Windows DWM). r=vlad
authorAvi Halachmi <avihpit@yahoo.com>
Wed, 29 May 2013 13:31:16 +0300
changeset 133452 fcb0d844e1c4
parent 133451 4b2454694ed4
child 133453 032888440564
push id28770
push userahalachmi@mozilla.com
push date2013-05-30 13:13 +0000
treeherdermozilla-inbound@fcb0d844e1c4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvlad
bugs856427
milestone24.0a1
Bug 856427: Add vsync support at the refresh driver (Windows DWM). r=vlad
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -17,16 +17,19 @@
  * implement things like blocking on vsync.
  */
 
 #ifdef XP_WIN
 #include <windows.h>
 // mmsystem isn't part of WIN32_LEAN_AND_MEAN, so we have
 // to manually include it
 #include <mmsystem.h>
+
+#include <dwmapi.h>
+typedef HRESULT (WINAPI*DwmGetCompositionTimingInfoProc)(HWND hWnd, DWM_TIMING_INFO *info);
 #endif
 
 #include "mozilla/Util.h"
 
 #include "nsRefreshDriver.h"
 #include "nsITimer.h"
 #include "nsLayoutUtils.h"
 #include "nsPresContext.h"
@@ -295,16 +298,166 @@ protected:
     // then schedule the timer
     LOG("[%p] scheduling callback for %d ms (2)", this, delay);
     mTimer->InitWithFuncCallback(TimerTick, this, delay, nsITimer::TYPE_ONE_SHOT);
 
     mTargetTime = newTarget;
   }
 };
 
+#ifdef XP_WIN
+/*
+ * Uses vsync timing on windows with DWM. Falls back dynamically to fixed rate if required.
+ * - Call LoadDll() before usage and UnloadDll() when done (static, nesting unsupported)
+ */
+class PreciseRefreshDriverTimerWindowsDwmVsync :
+  public PreciseRefreshDriverTimer
+{
+public:
+  static void LoadDll()
+  {
+    if (sDwmGetCompositionTimingInfoPtr) {
+      return; // Already loaded.
+    }
+
+    sDwmDll = ::LoadLibraryW(L"dwmapi.dll");
+    if (sDwmDll) {
+      sDwmGetCompositionTimingInfoPtr = (DwmGetCompositionTimingInfoProc)::GetProcAddress(sDwmDll, "DwmGetCompositionTimingInfo");
+    }
+
+    if (!sDwmDll || !sDwmGetCompositionTimingInfoPtr) {
+      UnloadDll();
+    }
+  }
+
+  // Checks if the vsync API is accessible.
+  // Return value is meaningful after calling LoadDll() and before UnloadDll(), and false otherwise.
+  // Even when supported, API calls could still fail when DWM is disabled (can change at runtime)
+  static bool IsSupported()
+  {
+    return sDwmGetCompositionTimingInfoPtr ? true : false;
+  }
+
+  // OK to call even if never loaded and/or if load failed.
+  static void UnloadDll()
+  {
+    if (sDwmDll) {
+      FreeLibrary(sDwmDll);
+    }
+    sDwmDll = nullptr;
+    sDwmGetCompositionTimingInfoPtr = nullptr;
+  }
+
+  PreciseRefreshDriverTimerWindowsDwmVsync(double aRate, bool aPreferHwTiming = false)
+    : PreciseRefreshDriverTimer(aRate)
+    , mPreferHwTiming(aPreferHwTiming)
+  {
+  }
+
+protected:
+  // Indicates we should try to adjust to the HW's timing (get rate from the OS or use vsync)
+  // This is typically true if the default refresh-rate value was not modified by the user.
+  bool mPreferHwTiming;
+
+  nsresult GetVBlankInfo(mozilla::TimeStamp &aLastVBlank, mozilla::TimeDuration &aInterval)
+  {
+    MOZ_ASSERT(sDwmGetCompositionTimingInfoPtr, "DwmGetCompositionTimingInfoPtr is unavailable (windows vsync)");
+
+    DWM_TIMING_INFO timingInfo;
+    timingInfo.cbSize = sizeof(DWM_TIMING_INFO);
+    HRESULT hr = sDwmGetCompositionTimingInfoPtr(0, &timingInfo); // For the desktop window instead of a specific one.
+
+    if (FAILED(hr)) {
+      // This happens first time this is called.
+      return NS_ERROR_NOT_INITIALIZED;
+    }
+
+    LARGE_INTEGER time, freq;
+    ::QueryPerformanceCounter(&time);
+    ::QueryPerformanceFrequency(&freq);
+    aLastVBlank = TimeStamp::Now();
+    double secondsPassed = double(time.QuadPart - timingInfo.qpcVBlank) / double(freq.QuadPart);
+
+    aLastVBlank -= TimeDuration::FromSeconds(secondsPassed);
+    aInterval = TimeDuration::FromSeconds(double(timingInfo.qpcRefreshPeriod) / double(freq.QuadPart));
+
+    return NS_OK;
+  }
+
+  virtual void ScheduleNextTick(TimeStamp aNowTime)
+  {
+    static const TimeDuration kMinSaneInterval = TimeDuration::FromMilliseconds(3); // 330Hz
+    static const TimeDuration kMaxSaneInterval = TimeDuration::FromMilliseconds(44); // 23Hz
+    static const TimeDuration kNegativeMaxSaneInterval = TimeDuration::FromMilliseconds(-44); // Saves conversions for abs interval
+    TimeStamp lastVblank;
+    TimeDuration vblankInterval;
+
+    if (!mPreferHwTiming ||
+        NS_OK != GetVBlankInfo(lastVblank, vblankInterval) ||
+        vblankInterval > kMaxSaneInterval ||
+        vblankInterval < kMinSaneInterval ||
+        (aNowTime - lastVblank) > kMaxSaneInterval ||
+        (aNowTime - lastVblank) < kNegativeMaxSaneInterval) {
+      // Use the default timing without vsync
+      PreciseRefreshDriverTimer::ScheduleNextTick(aNowTime);
+      return;
+    }
+
+    TimeStamp newTarget = lastVblank + vblankInterval; // Base target
+
+    // However, timer callback might return early (or late, but that wouldn't bother us), and vblankInterval
+    // appears to be slightly (~1%) different on each call (probably the OS measuring recent actual interval[s])
+    // and since we don't want to re-target the same vsync, we keep advancing in vblank intervals until we find the
+    // next safe target (next vsync, but not within 10% interval of previous target).
+    // This is typically 0 or 1 iteration:
+    // If we're too early, next vsync would be the one we've already targeted (1 iteration).
+    // If the timer returned late, no iteration will be required.
+
+    const double kSameVsyncThreshold = 0.1;
+    while (newTarget <= mTargetTime + vblankInterval.MultDouble(kSameVsyncThreshold)) {
+      newTarget += vblankInterval;
+    }
+
+    // To make sure we always hit the same "side" of the signal:
+    // round the delay up (by adding 1, since we later floor) and add a little (10% by default).
+    // Note that newTarget doesn't change (and is the next vblank) as a reference when we're back.
+    static const double kDefaultPhaseShiftPercent = 10;
+    static const double phaseShiftFactor = 0.01 *
+      (Preferences::GetInt("layout.frame_rate.vsync.phasePercentage", kDefaultPhaseShiftPercent) % 100);
+
+    double phaseDelay = 1.0 + vblankInterval.ToMilliseconds() * phaseShiftFactor;
+
+    // ms until the next time we should tick
+    double delayMs = (newTarget - aNowTime).ToMilliseconds() + phaseDelay;
+
+    // Make sure the delay is never negative.
+    uint32_t delay = static_cast<uint32_t>(delayMs < 0 ? 0 : delayMs);
+
+    // log info & lateness
+    LOG("[%p] precise dwm-vsync timer last tick late by %f ms, next tick in %d ms",
+        this,
+        (aNowTime - mTargetTime).ToMilliseconds(),
+        delay);
+
+    // then schedule the timer
+    LOG("[%p] scheduling callback for %d ms (2)", this, delay);
+    mTimer->InitWithFuncCallback(TimerTick, this, delay, nsITimer::TYPE_ONE_SHOT);
+
+    mTargetTime = newTarget;
+  }
+
+private:
+  static HMODULE sDwmDll;
+  static DwmGetCompositionTimingInfoProc sDwmGetCompositionTimingInfoPtr;
+};
+
+HMODULE PreciseRefreshDriverTimerWindowsDwmVsync::sDwmDll = nullptr;
+DwmGetCompositionTimingInfoProc PreciseRefreshDriverTimerWindowsDwmVsync::sDwmGetCompositionTimingInfoPtr = nullptr;
+#endif
+
 /*
  * A RefreshDriverTimer for inactive documents.  When a new refresh driver is
  * added, the rate is reset to the base (normally 1s/1fps).  Every time
  * it ticks, a single refresh driver is poked.  Once they have all been poked,
  * the duration between ticks doubles, up to mDisableAfterMilliseconds.  At that point,
  * the timer is quiet and doesn't tick (until something is added to it again).
  *
  * When a timer is removed, there is a possibility of another timer
@@ -453,16 +606,20 @@ static InactiveRefreshDriverTimer *sThro
 static int32_t sHighPrecisionTimerRequests = 0;
 // a bare pointer to avoid introducing a static constructor
 static nsITimer *sDisableHighPrecisionTimersTimer = nullptr;
 #endif
 
 /* static */ void
 nsRefreshDriver::InitializeStatics()
 {
+#ifdef XP_WIN
+  PreciseRefreshDriverTimerWindowsDwmVsync::LoadDll();
+#endif
+
 #ifdef PR_LOGGING
   if (!gLog) {
     gLog = PR_NewLogModule("nsRefreshDriver");
   }
 #endif
 }
 
 /* static */ void
@@ -471,41 +628,50 @@ nsRefreshDriver::Shutdown()
   // clean up our timers
   delete sRegularRateTimer;
   delete sThrottledRateTimer;
 
   sRegularRateTimer = nullptr;
   sThrottledRateTimer = nullptr;
 
 #ifdef XP_WIN
+  PreciseRefreshDriverTimerWindowsDwmVsync::UnloadDll();
+
   if (sDisableHighPrecisionTimersTimer) {
     sDisableHighPrecisionTimersTimer->Cancel();
     NS_RELEASE(sDisableHighPrecisionTimersTimer);
     timeEndPeriod(1);
   } else if (sHighPrecisionTimerRequests) {
     timeEndPeriod(1);
   }
 #endif
 }
 
 /* static */ int32_t
 nsRefreshDriver::DefaultInterval()
 {
   return NSToIntRound(1000.0 / DEFAULT_FRAME_RATE);
 }
 
-// Compute the interval to use for the refresh driver timer, in
-// milliseconds
+// Compute the interval to use for the refresh driver timer, in milliseconds.
+// outIsDefault indicates that rate was not explicitly set by the user
+// so we might choose other, more appropriate rates (e.g. vsync, etc)
 double
-nsRefreshDriver::GetRegularTimerInterval() const
+nsRefreshDriver::GetRegularTimerInterval(bool *outIsDefault) const
 {
   int32_t rate = Preferences::GetInt("layout.frame_rate", -1);
   if (rate <= 0) {
-    // TODO: get the rate from the platform
     rate = DEFAULT_FRAME_RATE;
+    if (outIsDefault) {
+      *outIsDefault = true;
+    }
+  } else {
+    if (outIsDefault) {
+      *outIsDefault = false;
+    }
   }
   return 1000.0 / rate;
 }
 
 double
 nsRefreshDriver::GetThrottledTimerInterval() const
 {
   int32_t rate = Preferences::GetInt("layout.throttled_frame_rate", -1);
@@ -526,18 +692,28 @@ nsRefreshDriver::ChooseTimer() const
 {
   if (mThrottled) {
     if (!sThrottledRateTimer) 
       sThrottledRateTimer = new InactiveRefreshDriverTimer(GetThrottledTimerInterval(),
                                                            DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS * 1000.0);
     return sThrottledRateTimer;
   }
 
-  if (!sRegularRateTimer)
-    sRegularRateTimer = new PreciseRefreshDriverTimer(GetRegularTimerInterval());
+  if (!sRegularRateTimer) {
+    bool isDefault = true;
+    double rate = GetRegularTimerInterval(&isDefault);
+#ifdef XP_WIN
+    if (PreciseRefreshDriverTimerWindowsDwmVsync::IsSupported()) {
+      sRegularRateTimer = new PreciseRefreshDriverTimerWindowsDwmVsync(rate, isDefault);
+    }
+#endif
+    if (!sRegularRateTimer) {
+      sRegularRateTimer = new PreciseRefreshDriverTimer(rate);
+    }
+  }
   return sRegularRateTimer;
 }
 
 nsRefreshDriver::nsRefreshDriver(nsPresContext* aPresContext)
   : mActiveTimer(nullptr),
     mPresContext(aPresContext),
     mFrozen(false),
     mThrottled(false),
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -272,17 +272,17 @@ private:
                                            void* aUserArg);
   static PLDHashOperator BeginRefreshingImages(nsISupportsHashKey* aEntry,
                                                void* aUserArg);
   ObserverArray& ArrayFor(mozFlushType aFlushType);
   // Trigger a refresh immediately, if haven't been disconnected or frozen.
   void DoRefresh();
 
   double GetRefreshTimerInterval() const;
-  double GetRegularTimerInterval() const;
+  double GetRegularTimerInterval(bool *outIsDefault = nullptr) const;
   double GetThrottledTimerInterval() const;
 
   bool HaveFrameRequestCallbacks() const {
     return mFrameRequestCallbackDocs.Length() != 0;
   }
 
   mozilla::RefreshDriverTimer* ChooseTimer() const;
   mozilla::RefreshDriverTimer *mActiveTimer;