Bug 717872 - Move all image animation logic into a new class, FrameAnimator, and use it from RasterImage. r=seth
authorJoe Drew <joe@drew.ca>
Mon, 15 Jul 2013 14:38:59 -0400
changeset 150823 ea8d855c4edb57c2c45cdda9d67168701e804363
parent 150822 bf847b1a3776f8f057a6815ae8639216805cebd8
child 150824 638ccf9872d8aca1569fe56d5565906990b184f2
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersseth
bugs717872
milestone25.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 717872 - Move all image animation logic into a new class, FrameAnimator, and use it from RasterImage. r=seth This patch moves the logic of moving from one frame to another (and tracking what frame is current, etc) to a separate class, FrameAnimator. Deciding *whether* to animate, and actually calling that animation code, is left to RasterImage, but the animation itself is driven by FrameAnimator.
image/src/FrameAnimator.cpp
image/src/FrameAnimator.h
image/src/RasterImage.cpp
image/src/RasterImage.h
image/src/imgFrame.cpp
image/src/imgFrame.h
image/src/moz.build
new file mode 100644
--- /dev/null
+++ b/image/src/FrameAnimator.cpp
@@ -0,0 +1,264 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FrameAnimator.h"
+
+#include "imgIContainer.h"
+
+using namespace mozilla::image;
+using namespace mozilla;
+
+FrameAnimator::FrameAnimator(FrameBlender& aFrameBlender)
+  : mCurrentAnimationFrameIndex(0)
+  , mLoopCount(-1)
+  , mFrameBlender(aFrameBlender)
+  , mAnimationMode(imgIContainer::kNormalAnimMode)
+  , mDoneDecoding(false)
+{
+}
+
+uint32_t
+FrameAnimator::GetSingleLoopTime() const
+{
+  // If we aren't done decoding, we don't know the image's full play time.
+  if (!mDoneDecoding) {
+    return 0;
+  }
+
+  // If we're not looping, a single loop time has no meaning
+  if (mAnimationMode != imgIContainer::kNormalAnimMode) {
+    return 0;
+  }
+
+  uint32_t looptime = 0;
+  for (uint32_t i = 0; i < mFrameBlender.GetNumFrames(); ++i) {
+    int32_t timeout = mFrameBlender.RawGetFrame(i)->GetTimeout();
+    if (timeout > 0) {
+      looptime += static_cast<uint32_t>(timeout);
+    } else {
+      // If we have a frame that never times out, we're probably in an error
+      // case, but let's handle it more gracefully.
+      NS_WARNING("Negative frame timeout - how did this happen?");
+      return 0;
+    }
+  }
+
+  return looptime;
+}
+
+TimeStamp
+FrameAnimator::GetCurrentImgFrameEndTime() const
+{
+  imgFrame* currentFrame = mFrameBlender.RawGetFrame(mCurrentAnimationFrameIndex);
+  TimeStamp currentFrameTime = mCurrentAnimationFrameTime;
+  int64_t timeout = currentFrame->GetTimeout();
+
+  if (timeout < 0) {
+    // We need to return a sentinel value in this case, because our logic
+    // doesn't work correctly if we have a negative timeout value. The reason
+    // this positive infinity was chosen was because it works with the loop in
+    // RequestRefresh() below.
+    return TimeStamp() +
+           TimeDuration::FromMilliseconds(static_cast<double>(UINT64_MAX));
+  }
+
+  TimeDuration durationOfTimeout =
+    TimeDuration::FromMilliseconds(static_cast<double>(timeout));
+  TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
+
+  return currentFrameEndTime;
+}
+
+FrameAnimator::RefreshResult
+FrameAnimator::AdvanceFrame(TimeStamp aTime)
+{
+  NS_ASSERTION(aTime <= TimeStamp::Now(),
+               "Given time appears to be in the future");
+
+  uint32_t currentFrameIndex = mCurrentAnimationFrameIndex;
+  uint32_t nextFrameIndex = currentFrameIndex + 1;
+  uint32_t timeout = 0;
+
+  RefreshResult ret;
+
+  // If we're done decoding, we know we've got everything we're going to get.
+  // If we aren't, we only display fully-downloaded frames; everything else
+  // gets delayed.
+  bool needToWait = !mDoneDecoding &&
+                    mFrameBlender.RawGetFrame(nextFrameIndex) &&
+                    !mFrameBlender.RawGetFrame(nextFrameIndex)->ImageComplete();
+
+  if (needToWait) {
+    // Uh oh, the frame we want to show is currently being decoded (partial)
+    // Wait until the next refresh driver tick and try again
+    return ret;
+  } else {
+    // If we're done decoding the next frame, go ahead and display it now and
+    // reinit with the next frame's delay time.
+    if (mFrameBlender.GetNumFrames() == nextFrameIndex) {
+      // End of Animation, unless we are looping forever
+
+      // If animation mode is "loop once", it's time to stop animating
+      if (mAnimationMode == imgIContainer::kLoopOnceAnimMode || mLoopCount == 0) {
+        ret.animationFinished = true;
+      }
+
+      nextFrameIndex = 0;
+
+      if (mLoopCount > 0) {
+        mLoopCount--;
+      }
+
+      // If we're done, exit early.
+      if (ret.animationFinished) {
+        return ret;
+      }
+    }
+
+    timeout = mFrameBlender.GetFrame(nextFrameIndex)->GetTimeout();
+  }
+
+  // Bad data
+  if (!(timeout > 0)) {
+    ret.animationFinished = true;
+    ret.error = true;
+  }
+
+  if (nextFrameIndex == 0) {
+    ret.dirtyRect = mFirstFrameRefreshArea;
+  } else {
+    // Change frame
+    if (!mFrameBlender.DoBlend(&ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
+      // something went wrong, move on to next
+      NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed");
+      mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(true);
+      mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+      mCurrentAnimationFrameIndex = nextFrameIndex;
+
+      ret.error = true;
+      return ret;
+    }
+
+    mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(false);
+  }
+
+  mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+
+  // If we can get closer to the current time by a multiple of the image's loop
+  // time, we should.
+  uint32_t loopTime = GetSingleLoopTime();
+  if (loopTime > 0) {
+    TimeDuration delay = aTime - mCurrentAnimationFrameTime;
+    if (delay.ToMilliseconds() > loopTime) {
+      // Explicitly use integer division to get the floor of the number of
+      // loops.
+      uint32_t loops = static_cast<uint32_t>(delay.ToMilliseconds()) / loopTime;
+      mCurrentAnimationFrameTime += TimeDuration::FromMilliseconds(loops * loopTime);
+    }
+  }
+
+  // Set currentAnimationFrameIndex at the last possible moment
+  mCurrentAnimationFrameIndex = nextFrameIndex;
+
+  // If we're here, we successfully advanced the frame.
+  ret.frameAdvanced = true;
+
+  return ret;
+}
+
+FrameAnimator::RefreshResult
+FrameAnimator::RequestRefresh(const mozilla::TimeStamp& aTime)
+{
+  // only advance the frame if the current time is greater than or
+  // equal to the current frame's end time.
+  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime();
+
+  // By default, an empty RefreshResult.
+  RefreshResult ret;
+
+  while (currentFrameEndTime <= aTime) {
+    TimeStamp oldFrameEndTime = currentFrameEndTime;
+
+    RefreshResult frameRes = AdvanceFrame(aTime);
+
+    // Accumulate our result for returning to callers.
+    ret.Accumulate(frameRes);
+
+    currentFrameEndTime = GetCurrentImgFrameEndTime();
+
+    // if we didn't advance a frame, and our frame end time didn't change,
+    // then we need to break out of this loop & wait for the frame(s)
+    // to finish downloading
+    if (!frameRes.frameAdvanced && (currentFrameEndTime == oldFrameEndTime)) {
+      break;
+    }
+  }
+
+  return ret;
+}
+
+void
+FrameAnimator::ResetAnimation()
+{
+  mCurrentAnimationFrameIndex = 0;
+}
+
+void
+FrameAnimator::SetDoneDecoding(bool aDone)
+{
+  mDoneDecoding = aDone;
+}
+
+void
+FrameAnimator::SetAnimationMode(uint16_t aAnimationMode)
+{
+  mAnimationMode = aAnimationMode;
+}
+
+void
+FrameAnimator::InitAnimationFrameTimeIfNecessary()
+{
+  if (mCurrentAnimationFrameTime.IsNull()) {
+    mCurrentAnimationFrameTime = TimeStamp::Now();
+  }
+}
+
+void
+FrameAnimator::SetAnimationFrameTime(const TimeStamp& aTime)
+{
+  mCurrentAnimationFrameTime = aTime;
+}
+
+void
+FrameAnimator::SetFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea = aRect;
+}
+
+void
+FrameAnimator::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
+}
+
+void
+FrameAnimator::SetLoopCount(int loopcount)
+{
+  mLoopCount = loopcount;
+}
+
+uint32_t
+FrameAnimator::GetCurrentAnimationFrameIndex() const
+{
+  return mCurrentAnimationFrameIndex;
+}
+
+nsIntRect
+FrameAnimator::GetFirstFrameRefreshArea() const
+{
+  return mFirstFrameRefreshArea;
+}
+
+
new file mode 100644
--- /dev/null
+++ b/image/src/FrameAnimator.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_imagelib_FrameAnimator_h_
+#define mozilla_imagelib_FrameAnimator_h_
+
+#include "mozilla/TimeStamp.h"
+#include "FrameBlender.h"
+#include "nsRect.h"
+
+namespace mozilla {
+namespace image {
+
+class FrameAnimator
+{
+public:
+  FrameAnimator(FrameBlender& aBlender);
+
+  /**
+   * Return value from RequestRefresh. Tells callers what happened in that call
+   * to RequestRefresh.
+   */
+  struct RefreshResult
+  {
+    // The dirty rectangle to be re-drawn after this RequestRefresh().
+    nsIntRect dirtyRect;
+
+    // Whether any frame changed, and hence the dirty rect was set.
+    bool frameAdvanced : 1;
+
+    // Whether the animation has finished playing.
+    bool animationFinished : 1;
+
+    // Whether an error has occurred when trying to advance a frame. Note that
+    // errors do not, on their own, end the animation.
+    bool error : 1;
+
+    RefreshResult()
+      : frameAdvanced(false)
+      , animationFinished(false)
+      , error(false)
+    {}
+
+    void Accumulate(const RefreshResult& other)
+    {
+      frameAdvanced = frameAdvanced || other.frameAdvanced;
+      animationFinished = animationFinished || other.animationFinished;
+      error = error || other.error;
+      dirtyRect = dirtyRect.Union(other.dirtyRect);
+    }
+  };
+
+  /**
+   * Re-evaluate what frame we're supposed to be on, and do whatever blending
+   * is necessary to get us to that frame.
+   *
+   * Returns the result of that blending, including whether the current frame
+   * changed and what the resulting dirty rectangle is.
+   */
+  RefreshResult RequestRefresh(const mozilla::TimeStamp& aTime);
+
+  /**
+   * Call when this image is finished decoding so we know that there aren't any
+   * more frames coming.
+   */
+  void SetDoneDecoding(bool aDone);
+
+  /**
+   * Call when you need to re-start animating. Ensures we start from the first
+   * frame.
+   */
+  void ResetAnimation();
+
+  /**
+   * Number of times to loop the image.
+   * @note -1 means forever.
+   */
+  void SetLoopCount(int32_t aLoopCount);
+
+  /**
+   * The animation mode of the image.
+   *
+   * Constants defined in imgIContainer.idl.
+   */
+  void SetAnimationMode(uint16_t aAnimationMode);
+
+  /**
+   * Set the area to refresh when we loop around to the first frame.
+   */
+  void SetFirstFrameRefreshArea(const nsIntRect& aRect);
+
+  /**
+   * Union the area to refresh when we loop around to the first frame with this
+   * rect.
+   */
+  void UnionFirstFrameRefreshArea(const nsIntRect& aRect);
+
+  /**
+   * If the animation frame time has not yet been set, set it to
+   * TimeStamp::Now().
+   */
+  void InitAnimationFrameTimeIfNecessary();
+
+  /**
+   * Set the animation frame time to @aTime.
+   */
+  void SetAnimationFrameTime(const TimeStamp& aTime);
+
+  /**
+   * The current frame we're on, from 0 to (numFrames - 1).
+   */
+  uint32_t GetCurrentAnimationFrameIndex() const;
+
+  /**
+   * Get the area we refresh when we loop around to the first frame.
+   */
+  nsIntRect GetFirstFrameRefreshArea() const;
+
+private: // methods
+  /**
+   * Gets the length of a single loop of this image, in milliseconds.
+   *
+   * If this image is not finished decoding, is not animated, or it is animated
+   * but does not loop, returns 0.
+   */
+  uint32_t GetSingleLoopTime() const;
+
+  /**
+   * Advances the animation. Typically, this will advance a single frame, but it
+   * may advance multiple frames. This may happen if we have infrequently
+   * "ticking" refresh drivers (e.g. in background tabs), or extremely short-
+   * lived animation frames.
+   *
+   * @param aTime the time that the animation should advance to. This will
+   *              typically be <= TimeStamp::Now().
+   *
+   * @returns a RefreshResult that shows whether the frame was successfully
+   *          advanced, and its resulting dirty rect.
+   */
+  RefreshResult AdvanceFrame(mozilla::TimeStamp aTime);
+
+  /**
+   * Get the time the frame we're currently displaying is supposed to end.
+   *
+   * In the error case, returns an "infinity" timestamp.
+   */
+  mozilla::TimeStamp GetCurrentImgFrameEndTime() const;
+
+private: // data
+  //! Area of the first frame that needs to be redrawn on subsequent loops.
+  nsIntRect mFirstFrameRefreshArea;
+
+  //! the time that the animation advanced to the current frame
+  TimeStamp mCurrentAnimationFrameTime;
+
+  //! The current frame index we're on. 0 to (numFrames - 1).
+  uint32_t mCurrentAnimationFrameIndex;
+
+  //! number of loops remaining before animation stops (-1 no stop)
+  int32_t mLoopCount;
+
+  //! All the frames of the image, shared with our owner
+  FrameBlender& mFrameBlender;
+
+  //! The animation mode of this image. Constants defined in imgIContainer.
+  uint16_t mAnimationMode;
+
+  //! Whether this image is done being decoded.
+  bool mDoneDecoding;
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif /* mozilla_imagelib_FrameAnimator_h_ */
--- a/image/src/RasterImage.cpp
+++ b/image/src/RasterImage.cpp
@@ -19,16 +19,17 @@
 #include "prsystem.h"
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "nsPresContext.h"
 #include "nsThread.h"
 #include "nsIThreadPool.h"
 #include "nsXPCOMCIDInternal.h"
 #include "nsIObserverService.h"
+#include "FrameAnimator.h"
 
 #include "nsPNGDecoder.h"
 #include "nsGIFDecoder2.h"
 #include "nsJPEGDecoder.h"
 #include "nsBMPDecoder.h"
 #include "nsICODecoder.h"
 #include "nsIconDecoder.h"
 
@@ -385,17 +386,16 @@ NS_IMPL_THREADSAFE_ISUPPORTS3(RasterImag
 //******************************************************************************
 RasterImage::RasterImage(imgStatusTracker* aStatusTracker,
                          nsIURI* aURI /* = nullptr */) :
   ImageResource(aStatusTracker, aURI), // invoke superclass's constructor
   mSize(0,0),
   mFrameDecodeFlags(DECODE_FLAGS_DEFAULT),
   mMultipartDecodedFrame(nullptr),
   mAnim(nullptr),
-  mLoopCount(-1),
   mLockCount(0),
   mDecodeCount(0),
 #ifdef DEBUG
   mFramesNotified(0),
 #endif
   mDecodingMutex("RasterImage"),
   mDecoder(nullptr),
   mBytesDecoded(0),
@@ -523,197 +523,51 @@ RasterImage::Init(const char* aMimeType,
   }
 
   // Mark us as initialized
   mInitialized = true;
 
   return NS_OK;
 }
 
-uint32_t
-RasterImage::GetSingleLoopTime() const
-{
-  if (!mAnim) {
-    return 0;
-  }
-
-  // If we aren't done decoding, we don't know the image's full play time.
-  if (!mHasBeenDecoded) {
-    return 0;
-  }
-
-  // If we're not looping, a single loop time has no meaning
-  if (mLoopCount == 0) {
-    return 0;
-  }
-
-  uint32_t looptime = 0;
-  for (uint32_t i = 0; i < GetNumFrames(); ++i) {
-    int32_t timeout = mFrameBlender.RawGetFrame(i)->GetTimeout();
-    if (timeout > 0) {
-      looptime += static_cast<uint32_t>(timeout);
-    } else {
-      // If we have a frame that never times out, we're probably in an error
-      // case, but let's handle it more gracefully.
-      NS_WARNING("Negative frame timeout - how did this happen?");
-      return 0;
-    }
-  }
-
-  return looptime;
-}
-
-bool
-RasterImage::AdvanceFrame(TimeStamp aTime, nsIntRect* aDirtyRect)
-{
-  NS_ASSERTION(aTime <= TimeStamp::Now(),
-               "Given time appears to be in the future");
-
-  uint32_t currentFrameIndex = mAnim->currentAnimationFrameIndex;
-  uint32_t nextFrameIndex = mAnim->currentAnimationFrameIndex + 1;
-  uint32_t timeout = 0;
-
-  // Figure out if we have the next full frame. This is more complicated than
-  // just checking GetNumFrames() because decoders append their frames
-  // before they're filled in.
-  NS_ABORT_IF_FALSE(mDecoder || nextFrameIndex <= GetNumFrames(),
-                    "How did we get 2 indices too far by incrementing?");
-
-  // If we don't have a decoder, we know we've got everything we're going to
-  // get. If we do, we only display fully-downloaded frames; everything else
-  // gets delayed.
-  bool haveFullNextFrame = (mMultipart && mBytesDecoded == 0) || !mDecoder ||
-                            nextFrameIndex < mDecoder->GetCompleteFrameCount();
-
-  // If we're done decoding the next frame, go ahead and display it now and
-  // reinit with the next frame's delay time.
-  if (haveFullNextFrame) {
-    if (GetNumFrames() == nextFrameIndex) {
-      // End of Animation, unless we are looping forever
-
-      // If animation mode is "loop once", it's time to stop animating
-      if (mAnimationMode == kLoopOnceAnimMode || mLoopCount == 0) {
-        mAnimationFinished = true;
-        EvaluateAnimation();
-      }
-
-      nextFrameIndex = 0;
-
-      if (mLoopCount > 0) {
-        mLoopCount--;
-      }
-
-      if (!mAnimating) {
-        // break out early if we are actually done animating
-        return false;
-      }
-    }
-
-    timeout = mFrameBlender.GetFrame(nextFrameIndex)->GetTimeout();
-
-  } else {
-    // Uh oh, the frame we want to show is currently being decoded (partial)
-    // Wait until the next refresh driver tick and try again
-    return false;
-  }
-
-  if (!(timeout > 0)) {
-    mAnimationFinished = true;
-    EvaluateAnimation();
-  }
-
-  if (nextFrameIndex == 0) {
-    *aDirtyRect = mAnim->firstFrameRefreshArea;
-  } else {
-    // Change frame
-    if (!mFrameBlender.DoBlend(aDirtyRect, currentFrameIndex, nextFrameIndex)) {
-      // something went wrong, move on to next
-      NS_WARNING("RasterImage::AdvanceFrame(): Compositing of frame failed");
-      mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(true);
-      mAnim->currentAnimationFrameTime = GetCurrentImgFrameEndTime();
-      mAnim->currentAnimationFrameIndex = nextFrameIndex;
-      return false;
-    }
-
-    mFrameBlender.RawGetFrame(nextFrameIndex)->SetCompositingFailed(false);
-  }
-
-  mAnim->currentAnimationFrameTime = GetCurrentImgFrameEndTime();
-
-  // If we can get closer to the current time by a multiple of the image's loop
-  // time, we should.
-  uint32_t loopTime = GetSingleLoopTime();
-  if (loopTime > 0) {
-    TimeDuration delay = aTime - mAnim->currentAnimationFrameTime;
-    if (delay.ToMilliseconds() > loopTime) {
-      // Explicitly use integer division to get the floor of the number of
-      // loops.
-      uint32_t loops = static_cast<uint32_t>(delay.ToMilliseconds()) / loopTime;
-      mAnim->currentAnimationFrameTime += TimeDuration::FromMilliseconds(loops * loopTime);
-    }
-  }
-
-  // Set currentAnimationFrameIndex at the last possible moment
-  mAnim->currentAnimationFrameIndex = nextFrameIndex;
-
-  return true;
-}
-
 //******************************************************************************
 // [notxpcom] void requestRefresh ([const] in TimeStamp aTime);
 NS_IMETHODIMP_(void)
 RasterImage::RequestRefresh(const mozilla::TimeStamp& aTime)
 {
   if (!ShouldAnimate()) {
     return;
   }
 
   EvaluateAnimation();
 
-  // only advance the frame if the current time is greater than or
-  // equal to the current frame's end time.
-  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime();
-  bool frameAdvanced = false;
-
-  // The dirtyRect variable will contain an accumulation of the sub-rectangles
-  // that are dirty for each frame we advance in AdvanceFrame().
-  nsIntRect dirtyRect;
-
-  while (currentFrameEndTime <= aTime) {
-    TimeStamp oldFrameEndTime = currentFrameEndTime;
-    nsIntRect frameDirtyRect;
-    bool didAdvance = AdvanceFrame(aTime, &frameDirtyRect);
-    frameAdvanced = frameAdvanced || didAdvance;
-    currentFrameEndTime = GetCurrentImgFrameEndTime();
-
-    // Accumulate the dirty area.
-    dirtyRect = dirtyRect.Union(frameDirtyRect);
-
-    // if we didn't advance a frame, and our frame end time didn't change,
-    // then we need to break out of this loop & wait for the frame(s)
-    // to finish downloading
-    if (!didAdvance && (currentFrameEndTime == oldFrameEndTime)) {
-      break;
-    }
+  FrameAnimator::RefreshResult res;
+  if (mAnim) {
+    res = mAnim->RequestRefresh(aTime);
   }
 
-  if (frameAdvanced) {
+  if (res.frameAdvanced) {
     // Notify listeners that our frame has actually changed, but do this only
     // once for all frames that we've now passed (if AdvanceFrame() was called
     // more than once).
     #ifdef DEBUG
       mFramesNotified++;
     #endif
 
     UpdateImageContainer();
 
     // Explicitly call this on mStatusTracker so we're sure to not interfere
     // with the decoding process
     if (mStatusTracker)
-      mStatusTracker->FrameChanged(&dirtyRect);
+      mStatusTracker->FrameChanged(&res.dirtyRect);
+  }
+
+  if (res.animationFinished) {
+    mAnimationFinished = true;
+    EvaluateAnimation();
   }
 }
 
 //******************************************************************************
 /* readonly attribute int32_t width; */
 NS_IMETHODIMP
 RasterImage::GetWidth(int32_t *aWidth)
 {
@@ -828,44 +682,21 @@ RasterImage::GetDrawableImgFrame(uint32_
     return nullptr;
   return frame;
 }
 
 uint32_t
 RasterImage::GetCurrentImgFrameIndex() const
 {
   if (mAnim)
-    return mAnim->currentAnimationFrameIndex;
+    return mAnim->GetCurrentAnimationFrameIndex();
 
   return 0;
 }
 
-TimeStamp
-RasterImage::GetCurrentImgFrameEndTime() const
-{
-  imgFrame* currentFrame = mFrameBlender.RawGetFrame(mAnim->currentAnimationFrameIndex);
-  TimeStamp currentFrameTime = mAnim->currentAnimationFrameTime;
-  int64_t timeout = currentFrame->GetTimeout();
-
-  if (timeout < 0) {
-    // We need to return a sentinel value in this case, because our logic
-    // doesn't work correctly if we have a negative timeout value. The reason
-    // this positive infinity was chosen was because it works with the loop in
-    // RequestRefresh() above.
-    return TimeStamp() +
-           TimeDuration::FromMilliseconds(static_cast<double>(UINT64_MAX));
-  }
-
-  TimeDuration durationOfTimeout =
-    TimeDuration::FromMilliseconds(static_cast<double>(timeout));
-  TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
-
-  return currentFrameEndTime;
-}
-
 imgFrame*
 RasterImage::GetCurrentImgFrame()
 {
   return GetImgFrame(GetCurrentImgFrameIndex());
 }
 
 //******************************************************************************
 /* [notxpcom] boolean frameIsOpaque(in uint32_t aWhichFrame); */
@@ -1225,16 +1056,40 @@ RasterImage::NonHeapSizeOfDecoded() cons
 
 size_t
 RasterImage::OutOfProcessSizeOfDecoded() const
 {
   return SizeOfDecodedWithComputedFallbackIfHeap(gfxASurface::MEMORY_OUT_OF_PROCESS,
                                                  NULL);
 }
 
+void
+RasterImage::EnsureAnimExists()
+{
+  if (!mAnim) {
+
+    // Create the animation context
+    mAnim = new FrameAnimator(mFrameBlender);
+
+    // We don't support discarding animated images (See bug 414259).
+    // Lock the image and throw away the key.
+    //
+    // Note that this is inefficient, since we could get rid of the source
+    // data too. However, doing this is actually hard, because we're probably
+    // calling ensureAnimExists mid-decode, and thus we're decoding out of
+    // the source buffer. Since we're going to fix this anyway later, and
+    // since we didn't kill the source data in the old world either, locking
+    // is acceptable for the moment.
+    LockImage();
+
+    // Notify our observers that we are starting animation.
+    CurrentStatusTracker().RecordImageIsAnimated();
+  }
+}
+
 nsresult
 RasterImage::InternalAddFrameHelper(uint32_t framenum, imgFrame *aFrame,
                                     uint8_t **imageData, uint32_t *imageLength,
                                     uint32_t **paletteData, uint32_t *paletteLength,
                                     imgFrame** aRetFrame)
 {
   NS_ABORT_IF_FALSE(framenum <= GetNumFrames(), "Invalid frame index!");
   if (framenum > GetNumFrames())
@@ -1305,25 +1160,23 @@ RasterImage::InternalAddFrame(uint32_t f
     EnsureAnimExists();
 
     // If we dispose of the first frame by clearing it, then the
     // First Frame's refresh area is all of itself.
     // RESTORE_PREVIOUS is invalid (assumed to be DISPOSE_CLEAR)
     int32_t frameDisposalMethod = mFrameBlender.RawGetFrame(0)->GetFrameDisposalMethod();
     if (frameDisposalMethod == FrameBlender::kDisposeClear ||
         frameDisposalMethod == FrameBlender::kDisposeRestorePrevious)
-      mAnim->firstFrameRefreshArea = mFrameBlender.RawGetFrame(0)->GetRect();
+      mAnim->SetFirstFrameRefreshArea(mFrameBlender.RawGetFrame(0)->GetRect());
   }
 
   // Calculate firstFrameRefreshArea
   // Some gifs are huge but only have a small area that they animate
   // We only need to refresh that small area when Frame 0 comes around again
-  nsIntRect frameRect = frame->GetRect();
-  mAnim->firstFrameRefreshArea.UnionRect(mAnim->firstFrameRefreshArea,
-                                         frameRect);
+  mAnim->UnionFirstFrameRefreshArea(frame->GetRect());
 
   rv = InternalAddFrameHelper(framenum, frame.forget(), imageData, imageLength,
                               paletteData, paletteLength, aRetFrame);
 
   return rv;
 }
 
 bool
@@ -1536,19 +1389,32 @@ RasterImage::DecodingComplete()
       // complexity and it's not really needed since we already are smart about
       // not displaying the still-decoding frame of an animated image. We may
       // have already stored an extra frame, though, so we'll release it here.
       delete mMultipartDecodedFrame;
       mMultipartDecodedFrame = nullptr;
     }
   }
 
+  if (mAnim) {
+    mAnim->SetDoneDecoding(true);
+  }
+
   return NS_OK;
 }
 
+NS_IMETHODIMP
+RasterImage::SetAnimationMode(uint16_t aAnimationMode)
+{
+  if (mAnim) {
+    mAnim->SetAnimationMode(aAnimationMode);
+  }
+  return SetAnimationModeInternal(aAnimationMode);
+}
+
 //******************************************************************************
 /* void StartAnimation () */
 nsresult
 RasterImage::StartAnimation()
 {
   if (mError)
     return NS_ERROR_FAILURE;
 
@@ -1560,19 +1426,17 @@ RasterImage::StartAnimation()
   if (currentFrame) {
     if (currentFrame->GetTimeout() < 0) { // -1 means display this frame forever
       mAnimationFinished = true;
       return NS_ERROR_ABORT;
     }
 
     // We need to set the time that this initial frame was first displayed, as
     // this is used in AdvanceFrame().
-    if (mAnim->currentAnimationFrameTime.IsNull()) {
-      mAnim->currentAnimationFrameTime = TimeStamp::Now();
-    }
+    mAnim->InitAnimationFrameTimeIfNecessary();
   }
 
   return NS_OK;
 }
 
 //******************************************************************************
 /* void stopAnimation (); */
 nsresult
@@ -1590,35 +1454,38 @@ RasterImage::StopAnimation()
 /* void resetAnimation (); */
 NS_IMETHODIMP
 RasterImage::ResetAnimation()
 {
   if (mError)
     return NS_ERROR_FAILURE;
 
   if (mAnimationMode == kDontAnimMode ||
-      !mAnim || mAnim->currentAnimationFrameIndex == 0)
+      !mAnim || mAnim->GetCurrentAnimationFrameIndex() == 0)
     return NS_OK;
 
   mAnimationFinished = false;
 
   if (mAnimating)
     StopAnimation();
 
   mFrameBlender.ResetAnimation();
-
-  mAnim->currentAnimationFrameIndex = 0;
+  if (mAnim) {
+    mAnim->ResetAnimation();
+  }
+
   UpdateImageContainer();
 
   // Note - We probably want to kick off a redecode somewhere around here when
   // we fix bug 500402.
 
   // Update display if we were animating before
   if (mAnimating && mStatusTracker) {
-    mStatusTracker->FrameChanged(&(mAnim->firstFrameRefreshArea));
+    nsIntRect rect = mAnim->GetFirstFrameRefreshArea();
+    mStatusTracker->FrameChanged(&rect);
   }
 
   if (ShouldAnimate()) {
     StartAnimation();
     // The animation may not have been running before, if mAnimationFinished
     // was false (before we changed it to true in this function). So, mark the
     // animation as running.
     mAnimating = true;
@@ -1630,39 +1497,37 @@ RasterImage::ResetAnimation()
 //******************************************************************************
 // [notxpcom] void requestRefresh ([const] in TimeStamp aTime);
 NS_IMETHODIMP_(void)
 RasterImage::SetAnimationStartTime(const mozilla::TimeStamp& aTime)
 {
   if (mError || mAnimating || !mAnim)
     return;
 
-  mAnim->currentAnimationFrameTime = aTime;
+  mAnim->SetAnimationFrameTime(aTime);
 }
 
 NS_IMETHODIMP_(float)
 RasterImage::GetFrameIndex(uint32_t aWhichFrame)
 {
   MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument");
   return (aWhichFrame == FRAME_FIRST || !mAnim)
          ? 0.0f
-         : mAnim->currentAnimationFrameIndex;
+         : mAnim->GetCurrentAnimationFrameIndex();
 }
 
 void
 RasterImage::SetLoopCount(int32_t aLoopCount)
 {
   if (mError)
     return;
 
-  // -1  infinite
-  //  0  no looping, one iteration
-  //  1  one loop, two iterations
-  //  ...
-  mLoopCount = aLoopCount;
+  if (mAnim) {
+    mAnim->SetLoopCount(aLoopCount);
+  }
 }
 
 nsresult
 RasterImage::AddSourceData(const char *aBuffer, uint32_t aCount)
 {
   MutexAutoLock lock(mDecodingMutex);
 
   if (mError)
@@ -1924,16 +1789,20 @@ RasterImage::OnNewSourceData()
 
   // Reset some flags
   mDecoded = false;
   mHasSourceData = false;
   mHasSize = false;
   mWantFullDecode = true;
   mDecodeRequest = nullptr;
 
+  if (mAnim) {
+    mAnim->SetDoneDecoding(false);
+  }
+
   // We always need the size first.
   rv = InitDecoder(/* aDoSizeDecode = */ true);
   CONTAINER_ENSURE_SUCCESS(rv);
 
   return NS_OK;
 }
 
 nsresult
--- a/image/src/RasterImage.h
+++ b/image/src/RasterImage.h
@@ -115,33 +115,30 @@ class nsIThreadPool;
  * disposal.  So, in the middle of DoComposite when composing Frame 3, right
  * after destroying Frame 2's area, we copy compositingFrame to
  * prevCompositingFrame.  When DoComposite gets called to do Frame 4, we
  * copy prevCompositingFrame back, and then draw Frame 4 on top.
  *
  * @par
  * The mAnim structure has members only needed for animated images, so
  * it's not allocated until the second frame is added.
- *
- * @note
- * mAnimationMode and mLoopCount are not in the mAnim structure because
- * they have public setters.
  */
 
 class ScaleRequest;
 
 namespace mozilla {
 namespace layers {
 class LayerManager;
 class ImageContainer;
 class Image;
 }
 namespace image {
 
 class Decoder;
+class FrameAnimator;
 
 class RasterImage : public ImageResource
                   , public nsIProperties
                   , public SupportsWeakPtr<RasterImage>
 #ifdef DEBUG
                   , public imgIContainerDebug
 #endif
 {
@@ -319,30 +316,16 @@ private:
       return *mDecodeRequest->mStatusTracker;
     } else {
       return *mStatusTracker;
     }
   }
 
   nsresult OnImageDataCompleteCore(nsIRequest* aRequest, nsISupports*, nsresult aStatus);
 
-  struct Anim
-  {
-    //! Area of the first frame that needs to be redrawn on subsequent loops.
-    nsIntRect                  firstFrameRefreshArea;
-    uint32_t                   currentAnimationFrameIndex; // 0 to numFrames-1
-
-    // the time that the animation advanced to the current frame
-    TimeStamp                  currentAnimationFrameTime;
-
-    Anim() :
-      currentAnimationFrameIndex(0)
-    {}
-  };
-
   /**
    * Each RasterImage has a pointer to one or zero heap-allocated
    * DecodeRequests.
    */
   struct DecodeRequest
   {
     DecodeRequest(RasterImage* aImage)
       : mImage(aImage)
@@ -541,84 +524,35 @@ private:
                                     const nsIntRect &aSubimage,
                                     uint32_t aFlags);
 
   nsresult CopyFrame(uint32_t aWhichFrame,
                      uint32_t aFlags,
                      gfxImageSurface **_retval);
 
   /**
-   * Advances the animation. Typically, this will advance a single frame, but it
-   * may advance multiple frames. This may happen if we have infrequently
-   * "ticking" refresh drivers (e.g. in background tabs), or extremely short-
-   * lived animation frames.
-   *
-   * @param aTime the time that the animation should advance to. This will
-   *              typically be <= TimeStamp::Now().
-   *
-   * @param [out] aDirtyRect a pointer to an nsIntRect which encapsulates the
-   *        area to be repainted after the frame is advanced.
-   *
-   * @returns true, if the frame was successfully advanced, false if it was not
-   *          able to be advanced (e.g. the frame to which we want to advance is
-   *          still decoding). Note: If false is returned, then aDirtyRect will
-   *          remain unmodified.
-   */
-  bool AdvanceFrame(mozilla::TimeStamp aTime, nsIntRect* aDirtyRect);
-
-  /**
-   * Gets the length of a single loop of this image, in milliseconds.
-   *
-   * If this image is not finished decoding, is not animated, or it is animated
-   * but does not loop, returns 0.
-   */
-  uint32_t GetSingleLoopTime() const;
-
-  /**
    * Deletes and nulls out the frame in mFrames[framenum].
    *
    * Does not change the size of mFrames.
    *
    * @param framenum The index of the frame to be deleted.
    *                 Must lie in [0, mFrames.Length() )
    */
   void DeleteImgFrame(uint32_t framenum);
 
   imgFrame* GetImgFrameNoDecode(uint32_t framenum);
   imgFrame* GetImgFrame(uint32_t framenum);
   imgFrame* GetDrawableImgFrame(uint32_t framenum);
   imgFrame* GetCurrentImgFrame();
   uint32_t GetCurrentImgFrameIndex() const;
-  mozilla::TimeStamp GetCurrentImgFrameEndTime() const;
 
   size_t SizeOfDecodedWithComputedFallbackIfHeap(gfxASurface::MemoryLocation aLocation,
                                                  mozilla::MallocSizeOf aMallocSizeOf) const;
 
-  inline void EnsureAnimExists()
-  {
-    if (!mAnim) {
-
-      // Create the animation context
-      mAnim = new Anim();
-
-      // We don't support discarding animated images (See bug 414259).
-      // Lock the image and throw away the key.
-      //
-      // Note that this is inefficient, since we could get rid of the source
-      // data too. However, doing this is actually hard, because we're probably
-      // calling ensureAnimExists mid-decode, and thus we're decoding out of
-      // the source buffer. Since we're going to fix this anyway later, and
-      // since we didn't kill the source data in the old world either, locking
-      // is acceptable for the moment.
-      LockImage();
-
-      // Notify our observers that we are starting animation.
-      CurrentStatusTracker().RecordImageIsAnimated();
-    }
-  }
+  void EnsureAnimExists();
 
   nsresult InternalAddFrameHelper(uint32_t framenum, imgFrame *frame,
                                   uint8_t **imageData, uint32_t *imageLength,
                                   uint32_t **paletteData, uint32_t *paletteLength,
                                   imgFrame** aRetFrame);
   nsresult InternalAddFrame(uint32_t framenum, int32_t aX, int32_t aY, int32_t aWidth, int32_t aHeight,
                             gfxASurface::gfxImageFormat aFormat, uint8_t aPaletteDepth,
                             uint8_t **imageData, uint32_t *imageLength,
@@ -666,20 +600,17 @@ private: // data
   // The last frame we decoded for multipart images.
   imgFrame*                  mMultipartDecodedFrame;
 
   nsCOMPtr<nsIProperties>    mProperties;
 
   // IMPORTANT: if you use mAnim in a method, call EnsureImageIsDecoded() first to ensure
   // that the frames actually exist (they may have been discarded to save memory, or
   // we maybe decoding on draw).
-  RasterImage::Anim*        mAnim;
-
-  //! # loops remaining before animation stops (-1 no stop)
-  int32_t                    mLoopCount;
+  FrameAnimator* mAnim;
 
   // Discard members
   uint32_t                   mLockCount;
   DiscardTracker::Node       mDiscardTrackerNode;
 
   // Source data members
   nsCString                  mSourceDataMimeType;
 
@@ -786,20 +717,16 @@ protected:
 
   friend class ImageFactory;
 };
 
 inline NS_IMETHODIMP RasterImage::GetAnimationMode(uint16_t *aAnimationMode) {
   return GetAnimationModeInternal(aAnimationMode);
 }
 
-inline NS_IMETHODIMP RasterImage::SetAnimationMode(uint16_t aAnimationMode) {
-  return SetAnimationModeInternal(aAnimationMode);
-}
-
 // Asynchronous Decode Requestor
 //
 // We use this class when someone calls requestDecode() from within a decode
 // notification. Since requestDecode() involves modifying the decoder's state
 // (for example, possibly shutting down a header-only decode and starting a
 // full decode), we don't want to do this from inside a decoder.
 class imgDecodeRequestor : public nsRunnable
 {
--- a/image/src/imgFrame.cpp
+++ b/image/src/imgFrame.cpp
@@ -504,17 +504,17 @@ nsresult imgFrame::ImageUpdated(const ns
   nsIntRect boundsRect(mOffset, mSize);
   mDecoded.IntersectRect(mDecoded, boundsRect);
 
   mDirty = true;
 
   return NS_OK;
 }
 
-bool imgFrame::GetIsDirty()
+bool imgFrame::GetIsDirty() const
 {
   MutexAutoLock lock(mDirtyMutex);
   return mDirty;
 }
 
 nsIntRect imgFrame::GetRect() const
 {
   return nsIntRect(mOffset, mSize);
@@ -791,18 +791,21 @@ int32_t imgFrame::GetBlendMethod() const
   return mBlendMethod;
 }
 
 void imgFrame::SetBlendMethod(int32_t aBlendMethod)
 {
   mBlendMethod = (int8_t)aBlendMethod;
 }
 
+// This can be called from any thread.
 bool imgFrame::ImageComplete() const
 {
+  MutexAutoLock lock(mDirtyMutex);
+
   return mDecoded.IsEqualInterior(nsIntRect(mOffset, mSize));
 }
 
 // A hint from the image decoders that this image has no alpha, even
 // though we created is ARGB32.  This changes our format to RGB24,
 // which in turn will cause us to Optimize() to RGB24.  Has no effect
 // after Optimize() is called, though in all cases it will be just a
 // performance win -- the pixels are still correct and have the A byte
--- a/image/src/imgFrame.h
+++ b/image/src/imgFrame.h
@@ -36,17 +36,17 @@ public:
   nsresult Optimize();
 
   void Draw(gfxContext *aContext, gfxPattern::GraphicsFilter aFilter,
             const gfxMatrix &aUserSpaceToImageSpace, const gfxRect& aFill,
             const nsIntMargin &aPadding, const nsIntRect &aSubimage,
             uint32_t aImageFlags = imgIContainer::FLAG_NONE);
 
   nsresult ImageUpdated(const nsIntRect &aUpdateRect);
-  bool GetIsDirty();
+  bool GetIsDirty() const;
 
   nsIntRect GetRect() const;
   gfxASurface::gfxImageFormat GetFormat() const;
   bool GetNeedsBackground() const;
   uint32_t GetImageBytesPerRow() const;
   uint32_t GetImageDataLength() const;
   bool GetIsPaletted() const;
   bool GetHasAlpha() const;
@@ -147,17 +147,17 @@ private: // data
   nsRefPtr<gfxQuartzImageSurface> mQuartzSurface;
 #endif
 
   nsIntSize    mSize;
   nsIntPoint   mOffset;
 
   nsIntRect    mDecoded;
 
-  mozilla::Mutex mDirtyMutex;
+  mutable mozilla::Mutex mDirtyMutex;
 
   // The palette and image data for images that are paletted, since Cairo
   // doesn't support these images.
   // The paletted data comes first, then the image data itself.
   // Total length is PaletteDataLength() + GetImageDataLength().
   uint8_t*     mPalettedImageData;
 
   // Note that the data stored in gfxRGBA is *non-alpha-premultiplied*.
--- a/image/src/moz.build
+++ b/image/src/moz.build
@@ -12,16 +12,17 @@ EXPORTS += [
     'imgRequest.h',
     'imgRequestProxy.h',
 ]
 
 CPP_SOURCES += [
     'ClippedImage.cpp',
     'Decoder.cpp',
     'DiscardTracker.cpp',
+    'FrameAnimator.cpp',
     'FrameBlender.cpp',
     'FrameSequence.cpp',
     'FrozenImage.cpp',
     'Image.cpp',
     'ImageFactory.cpp',
     'ImageMetadata.cpp',
     'ImageOps.cpp',
     'ImageWrapper.cpp',