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
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 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;