Bug 1288040 (Part 1) - Separate FrameAnimator's state into a separate class, AnimationState. r=edwin
authorSeth Fowler <mark.seth.fowler@gmail.com>
Mon, 18 Jul 2016 23:26:58 -0700
changeset 305955 8383c2cc99393e21a7b7f2f8129e94b0f9a77593
parent 305954 3d6cdeb53e16b0bc4fdb1f9d97b7ad9883132004
child 305956 34faad78d6f099f4d1a25a1c7935db5656fdf627
push id30474
push usercbook@mozilla.com
push dateThu, 21 Jul 2016 14:25:10 +0000
treeherdermozilla-central@6b180266ac16 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedwin
bugs1288040
milestone50.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 1288040 (Part 1) - Separate FrameAnimator's state into a separate class, AnimationState. r=edwin
image/FrameAnimator.cpp
image/FrameAnimator.h
image/RasterImage.cpp
image/RasterImage.h
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -15,51 +15,111 @@
 #include "pixman.h"
 
 namespace mozilla {
 
 using namespace gfx;
 
 namespace image {
 
+///////////////////////////////////////////////////////////////////////////////
+// AnimationState implementation.
+///////////////////////////////////////////////////////////////////////////////
+
+void
+AnimationState::SetDoneDecoding(bool aDone)
+{
+  mDoneDecoding = aDone;
+}
+
+void
+AnimationState::ResetAnimation()
+{
+  mCurrentAnimationFrameIndex = 0;
+  mLastCompositedFrameIndex = -1;
+}
+
+void
+AnimationState::SetAnimationMode(uint16_t aAnimationMode)
+{
+  mAnimationMode = aAnimationMode;
+}
+
+void
+AnimationState::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
+{
+  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
+}
+
+void
+AnimationState::InitAnimationFrameTimeIfNecessary()
+{
+  if (mCurrentAnimationFrameTime.IsNull()) {
+    mCurrentAnimationFrameTime = TimeStamp::Now();
+  }
+}
+
+void
+AnimationState::SetAnimationFrameTime(const TimeStamp& aTime)
+{
+  mCurrentAnimationFrameTime = aTime;
+}
+
+uint32_t
+AnimationState::GetCurrentAnimationFrameIndex() const
+{
+  return mCurrentAnimationFrameIndex;
+}
+
+nsIntRect
+AnimationState::GetFirstFrameRefreshArea() const
+{
+  return mFirstFrameRefreshArea;
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+// FrameAnimator implementation.
+///////////////////////////////////////////////////////////////////////////////
+
 int32_t
-FrameAnimator::GetSingleLoopTime() const
+FrameAnimator::GetSingleLoopTime(AnimationState& aState) const
 {
   // If we aren't done decoding, we don't know the image's full play time.
-  if (!mDoneDecoding) {
+  if (!aState.mDoneDecoding) {
     return -1;
   }
 
   // If we're not looping, a single loop time has no meaning
-  if (mAnimationMode != imgIContainer::kNormalAnimMode) {
+  if (aState.mAnimationMode != imgIContainer::kNormalAnimMode) {
     return -1;
   }
 
   int32_t looptime = 0;
   for (uint32_t i = 0; i < mImage->GetNumFrames(); ++i) {
-    int32_t timeout = GetTimeoutForFrame(i);
+    int32_t timeout = GetTimeoutForFrame(aState, i);
     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 -1;
     }
   }
 
   return looptime;
 }
 
 TimeStamp
-FrameAnimator::GetCurrentImgFrameEndTime() const
+FrameAnimator::GetCurrentImgFrameEndTime(AnimationState& aState) const
 {
-  TimeStamp currentFrameTime = mCurrentAnimationFrameTime;
+  TimeStamp currentFrameTime = aState.mCurrentAnimationFrameTime;
   int32_t timeout =
-    GetTimeoutForFrame(mCurrentAnimationFrameIndex);
+    GetTimeoutForFrame(aState, aState.mCurrentAnimationFrameIndex);
 
   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. We use
     // one year in the future as the sentinel because it works with the loop
     // in RequestRefresh() below.
     // XXX(seth): It'd be preferable to make our logic work correctly with
     // negative timeouts.
@@ -70,64 +130,64 @@ FrameAnimator::GetCurrentImgFrameEndTime
   TimeDuration durationOfTimeout =
     TimeDuration::FromMilliseconds(static_cast<double>(timeout));
   TimeStamp currentFrameEndTime = currentFrameTime + durationOfTimeout;
 
   return currentFrameEndTime;
 }
 
 FrameAnimator::RefreshResult
-FrameAnimator::AdvanceFrame(TimeStamp aTime)
+FrameAnimator::AdvanceFrame(AnimationState& aState, TimeStamp aTime)
 {
   NS_ASSERTION(aTime <= TimeStamp::Now(),
                "Given time appears to be in the future");
   PROFILER_LABEL_FUNC(js::ProfileEntry::Category::GRAPHICS);
 
   RefreshResult ret;
 
   // Determine what the next frame is, taking into account looping.
-  uint32_t currentFrameIndex = mCurrentAnimationFrameIndex;
+  uint32_t currentFrameIndex = aState.mCurrentAnimationFrameIndex;
   uint32_t nextFrameIndex = currentFrameIndex + 1;
 
   if (mImage->GetNumFrames() == nextFrameIndex) {
     // We can only accurately determine if we are at the end of the loop if we are
     // done decoding, otherwise we don't know how many frames there will be.
-    if (!mDoneDecoding) {
+    if (!aState.mDoneDecoding) {
       // We've already advanced to the last decoded frame, nothing more we can do.
       // We're blocked by network/decoding from displaying the animation at the
       // rate specified, so that means the frame we are displaying (the latest
       // available) is the frame we want to be displaying at this time. So we
       // update the current animation time. If we didn't update the current
       // animation time then it could lag behind, which would indicate that we
       // are behind in the animation and should try to catch up. When we are
       // done decoding (and thus can loop around back to the start of the
       // animation) we would then jump to a random point in the animation to
       // try to catch up. But we were never behind in the animation.
-      mCurrentAnimationFrameTime = aTime;
+      aState.mCurrentAnimationFrameTime = aTime;
       return ret;
     }
 
     // End of an animation loop...
 
     // If we are not looping forever, initialize the loop counter
-    if (mLoopRemainingCount < 0 && LoopCount() >= 0) {
-      mLoopRemainingCount = LoopCount();
+    if (aState.mLoopRemainingCount < 0 && aState.LoopCount() >= 0) {
+      aState.mLoopRemainingCount = aState.LoopCount();
     }
 
     // If animation mode is "loop once", or we're at end of loop counter,
     // it's time to stop animating.
-    if (mAnimationMode == imgIContainer::kLoopOnceAnimMode ||
-        mLoopRemainingCount == 0) {
+    if (aState.mAnimationMode == imgIContainer::kLoopOnceAnimMode ||
+        aState.mLoopRemainingCount == 0) {
       ret.animationFinished = true;
     }
 
     nextFrameIndex = 0;
 
-    if (mLoopRemainingCount > 0) {
-      mLoopRemainingCount--;
+    if (aState.mLoopRemainingCount > 0) {
+      aState.mLoopRemainingCount--;
     }
 
     // If we're done, exit early.
     if (ret.animationFinished) {
       return ret;
     }
   }
 
@@ -137,196 +197,145 @@ FrameAnimator::AdvanceFrame(TimeStamp aT
   // what RasterImage knows about so that we stay in sync with RasterImage. The code
   // above should obey this, the MOZ_ASSERT records this invariant.
   MOZ_ASSERT(nextFrameIndex < mImage->GetNumFrames());
   RawAccessFrameRef nextFrame = GetRawFrame(nextFrameIndex);
 
   // 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 canDisplay = mDoneDecoding ||
+  bool canDisplay = aState.mDoneDecoding ||
                     (nextFrame && nextFrame->IsFinished());
 
   if (!canDisplay) {
     // 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;
   }
 
   // Bad data
-  if (GetTimeoutForFrame(nextFrameIndex) < 0) {
+  if (GetTimeoutForFrame(aState, nextFrameIndex) < 0) {
     ret.animationFinished = true;
     ret.error = true;
   }
 
   if (nextFrameIndex == 0) {
-    ret.dirtyRect = mFirstFrameRefreshArea;
+    ret.dirtyRect = aState.mFirstFrameRefreshArea;
   } else {
     MOZ_ASSERT(nextFrameIndex == currentFrameIndex + 1);
 
     // Change frame
-    if (!DoBlend(&ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
+    if (!DoBlend(aState, &ret.dirtyRect, currentFrameIndex, nextFrameIndex)) {
       // something went wrong, move on to next
       NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed");
       nextFrame->SetCompositingFailed(true);
-      mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
-      mCurrentAnimationFrameIndex = nextFrameIndex;
+      aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState);
+      aState.mCurrentAnimationFrameIndex = nextFrameIndex;
 
       ret.error = true;
       return ret;
     }
 
     nextFrame->SetCompositingFailed(false);
   }
 
-  mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime();
+  aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState);
 
   // If we can get closer to the current time by a multiple of the image's loop
   // time, we should. We need to be done decoding in order to know the full loop
   // time though!
-  int32_t loopTime = GetSingleLoopTime();
+  int32_t loopTime = GetSingleLoopTime(aState);
   if (loopTime > 0) {
     // We shouldn't be advancing by a whole loop unless we are decoded and know
     // what a full loop actually is. GetSingleLoopTime should return -1 so this
     // never happens.
-    MOZ_ASSERT(mDoneDecoding);
-    TimeDuration delay = aTime - mCurrentAnimationFrameTime;
+    MOZ_ASSERT(aState.mDoneDecoding);
+    TimeDuration delay = aTime - aState.mCurrentAnimationFrameTime;
     if (delay.ToMilliseconds() > loopTime) {
       // Explicitly use integer division to get the floor of the number of
       // loops.
       uint64_t loops = static_cast<uint64_t>(delay.ToMilliseconds()) / loopTime;
-      mCurrentAnimationFrameTime +=
+      aState.mCurrentAnimationFrameTime +=
         TimeDuration::FromMilliseconds(loops * loopTime);
     }
   }
 
   // Set currentAnimationFrameIndex at the last possible moment
-  mCurrentAnimationFrameIndex = nextFrameIndex;
+  aState.mCurrentAnimationFrameIndex = nextFrameIndex;
 
   // If we're here, we successfully advanced the frame.
   ret.frameAdvanced = true;
 
   return ret;
 }
 
 FrameAnimator::RefreshResult
-FrameAnimator::RequestRefresh(const TimeStamp& aTime)
+FrameAnimator::RequestRefresh(AnimationState& aState, const 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();
+  TimeStamp currentFrameEndTime = GetCurrentImgFrameEndTime(aState);
 
   // By default, an empty RefreshResult.
   RefreshResult ret;
 
   while (currentFrameEndTime <= aTime) {
     TimeStamp oldFrameEndTime = currentFrameEndTime;
 
-    RefreshResult frameRes = AdvanceFrame(aTime);
+    RefreshResult frameRes = AdvanceFrame(aState, aTime);
 
     // Accumulate our result for returning to callers.
     ret.Accumulate(frameRes);
 
-    currentFrameEndTime = GetCurrentImgFrameEndTime();
+    currentFrameEndTime = GetCurrentImgFrameEndTime(aState);
 
     // 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;
-  mLastCompositedFrameIndex = -1;
-}
-
-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::UnionFirstFrameRefreshArea(const nsIntRect& aRect)
-{
-  mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, aRect);
-}
-
-uint32_t
-FrameAnimator::GetCurrentAnimationFrameIndex() const
-{
-  return mCurrentAnimationFrameIndex;
-}
-
-nsIntRect
-FrameAnimator::GetFirstFrameRefreshArea() const
-{
-  return mFirstFrameRefreshArea;
-}
-
 LookupResult
-FrameAnimator::GetCompositedFrame(uint32_t aFrameNum)
+FrameAnimator::GetCompositedFrame(AnimationState& aState, uint32_t aFrameNum)
 {
   MOZ_ASSERT(aFrameNum != 0, "First frame is never composited");
 
   // If we have a composited version of this frame, return that.
-  if (mLastCompositedFrameIndex == int32_t(aFrameNum)) {
+  if (aState.mLastCompositedFrameIndex == int32_t(aFrameNum)) {
     return LookupResult(mCompositingFrame->DrawableRef(), MatchType::EXACT);
   }
 
   // Otherwise return the raw frame. DoBlend is required to ensure that we only
   // hit this case if the frame is not paletted and doesn't require compositing.
   LookupResult result =
     SurfaceCache::Lookup(ImageKey(mImage),
                          RasterSurfaceKey(mSize,
                                           DefaultSurfaceFlags(),
                                           aFrameNum));
   MOZ_ASSERT(!result || !result.DrawableRef()->GetIsPaletted(),
              "About to return a paletted frame");
   return result;
 }
 
 int32_t
-FrameAnimator::GetTimeoutForFrame(uint32_t aFrameNum) const
+FrameAnimator::GetTimeoutForFrame(AnimationState& aState, uint32_t aFrameNum) const
 {
   int32_t rawTimeout = 0;
 
   RawAccessFrameRef frame = GetRawFrame(aFrameNum);
   if (frame) {
     AnimationData data = frame->GetAnimationData();
     rawTimeout = data.mRawTimeout;
   } else if (aFrameNum == 0) {
-    rawTimeout = mFirstFrameTimeout;
+    rawTimeout = aState.mFirstFrameTimeout;
   } else {
     NS_WARNING("No frame; called GetTimeoutForFrame too early?");
     return 100;
   }
 
   // Ensure a minimal time between updates so we don't throttle the UI thread.
   // consider 0 == unspecified and make it fast but not too fast.  Unless we
   // have a single loop GIF. See bug 890743, bug 125137, bug 139677, and bug
@@ -402,17 +411,18 @@ FrameAnimator::GetRawFrame(uint32_t aFra
   return result ? result.DrawableRef()->RawAccessRef()
                 : RawAccessFrameRef();
 }
 
 //******************************************************************************
 // DoBlend gets called when the timer for animation get fired and we have to
 // update the composited frame of the animation.
 bool
-FrameAnimator::DoBlend(nsIntRect* aDirtyRect,
+FrameAnimator::DoBlend(AnimationState& aState,
+                       nsIntRect* aDirtyRect,
                        uint32_t aPrevFrameIndex,
                        uint32_t aNextFrameIndex)
 {
   RawAccessFrameRef prevFrame = GetRawFrame(aPrevFrameIndex);
   RawAccessFrameRef nextFrame = GetRawFrame(aNextFrameIndex);
 
   MOZ_ASSERT(prevFrame && nextFrame, "Should have frames here");
 
@@ -489,34 +499,34 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
   }
 
   // Optimization:
   //   Skip compositing if the last composited frame is this frame
   //   (Only one composited frame was made for this animation.  Example:
   //    Only Frame 3 of a 10 frame image required us to build a composite frame
   //    On the second loop, we do not need to rebuild the frame
   //    since it's still sitting in compositingFrame)
-  if (mLastCompositedFrameIndex == int32_t(aNextFrameIndex)) {
+  if (aState.mLastCompositedFrameIndex == int32_t(aNextFrameIndex)) {
     return true;
   }
 
   bool needToBlankComposite = false;
 
   // Create the Compositing Frame
   if (!mCompositingFrame) {
     RefPtr<imgFrame> newFrame = new imgFrame;
     nsresult rv = newFrame->InitForDecoder(mSize,
                                            SurfaceFormat::B8G8R8A8);
     if (NS_FAILED(rv)) {
       mCompositingFrame.reset();
       return false;
     }
     mCompositingFrame = newFrame->RawAccessRef();
     needToBlankComposite = true;
-  } else if (int32_t(aNextFrameIndex) != mLastCompositedFrameIndex+1) {
+  } else if (int32_t(aNextFrameIndex) != aState.mLastCompositedFrameIndex+1) {
 
     // If we are not drawing on top of last composited frame,
     // then we are building a new composite frame, so let's clear it first.
     needToBlankComposite = true;
   }
 
   AnimationData compositingFrameData = mCompositingFrame->GetAnimationData();
 
@@ -600,17 +610,17 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
       case DisposalMethod::KEEP:
         // Copy previous frame into compositingFrame before we put the new
         // frame on top
         // Assumes that the previous frame represents a full frame (it could be
         // smaller in size than the container, as long as the frame before it
         // erased itself)
         // Note: Frame 1 never gets into DoBlend(), so (aNextFrameIndex - 1)
         // will always be a valid frame number.
-        if (mLastCompositedFrameIndex != int32_t(aNextFrameIndex - 1)) {
+        if (aState.mLastCompositedFrameIndex != int32_t(aNextFrameIndex - 1)) {
           if (isFullPrevFrame && !prevFrame->GetIsPaletted()) {
             // Just copy the bits
             CopyFrameImage(prevFrameData.mRawData,
                            prevFrameData.mRect,
                            compositingFrameData.mRawData,
                            compositingFrameData.mRect);
           } else {
             if (needToBlankComposite) {
@@ -675,17 +685,17 @@ FrameAnimator::DoBlend(nsIntRect* aDirty
               compositingFrameData.mRawData,
               compositingFrameData.mRect,
               nextFrameData.mBlendMethod,
               nextFrameData.mBlendRect);
 
   // Tell the image that it is fully 'downloaded'.
   mCompositingFrame->Finish();
 
-  mLastCompositedFrameIndex = int32_t(aNextFrameIndex);
+  aState.mLastCompositedFrameIndex = int32_t(aNextFrameIndex);
 
   return true;
 }
 
 //******************************************************************************
 // Fill aFrame with black. Does also clears the mask.
 void
 FrameAnimator::ClearFrame(uint8_t* aFrameData, const nsIntRect& aFrameRect)
--- a/image/FrameAnimator.h
+++ b/image/FrameAnimator.h
@@ -17,31 +17,125 @@
 #include "nsRect.h"
 #include "SurfaceCache.h"
 
 namespace mozilla {
 namespace image {
 
 class RasterImage;
 
-class FrameAnimator
+class AnimationState
 {
 public:
-  FrameAnimator(RasterImage* aImage,
-                gfx::IntSize aSize,
-                uint16_t aAnimationMode)
-    : mImage(aImage)
-    , mSize(aSize)
-    , mCurrentAnimationFrameIndex(0)
+  explicit AnimationState(uint16_t aAnimationMode)
+    : mCurrentAnimationFrameIndex(0)
+    , mLastCompositedFrameIndex(-1)
     , mLoopRemainingCount(-1)
-    , mLastCompositedFrameIndex(-1)
     , mLoopCount(-1)
     , mFirstFrameTimeout(0)
     , mAnimationMode(aAnimationMode)
     , mDoneDecoding(false)
+  { }
+
+  /**
+   * 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();
+
+  /**
+   * The animation mode of the image.
+   *
+   * Constants defined in imgIContainer.idl.
+   */
+  void SetAnimationMode(uint16_t aAnimationMode);
+
+  /**
+   * 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;
+
+  /*
+   * Set number of times to loop the image.
+   * @note -1 means loop forever.
+   */
+  void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; }
+  int32_t LoopCount() const { return mLoopCount; }
+
+  /*
+   * Set the timeout for the first frame. This is used to allow animation
+   * scheduling even before a full decode runs for this image.
+   */
+  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+
+private:
+  friend class FrameAnimator;
+
+  //! 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;
+
+  //! Track the last composited frame for Optimizations (See DoComposite code)
+  int32_t mLastCompositedFrameIndex;
+
+  //! number of loops remaining before animation stops (-1 no stop)
+  int32_t mLoopRemainingCount;
+
+  //! The total number of loops for the image.
+  int32_t mLoopCount;
+
+  //! The timeout for the first frame of this image.
+  int32_t mFirstFrameTimeout;
+
+  //! The animation mode of this image. Constants defined in imgIContainer.
+  uint16_t mAnimationMode;
+
+  //! Whether this image is done being decoded.
+  bool mDoneDecoding;
+};
+
+class FrameAnimator
+{
+public:
+  FrameAnimator(RasterImage* aImage, gfx::IntSize aSize)
+    : mImage(aImage)
+    , mSize(aSize)
   {
      MOZ_COUNT_CTOR(FrameAnimator);
   }
 
   ~FrameAnimator()
   {
     MOZ_COUNT_DTOR(FrameAnimator);
   }
@@ -82,138 +176,81 @@ public:
 
   /**
    * 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 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();
-
-  /**
-   * The animation mode of the image.
-   *
-   * Constants defined in imgIContainer.idl.
-   */
-  void SetAnimationMode(uint16_t aAnimationMode);
-
-  /**
-   * 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;
+  RefreshResult RequestRefresh(AnimationState& aState, const TimeStamp& aTime);
 
   /**
    * If we have a composited frame for @aFrameNum, returns it. Otherwise,
    * returns an empty LookupResult. It is an error to call this method with
    * aFrameNum == 0, because the first frame is never composited.
    */
-  LookupResult GetCompositedFrame(uint32_t aFrameNum);
+  LookupResult GetCompositedFrame(AnimationState& aState, uint32_t aFrameNum);
 
   /*
    * Returns the frame's adjusted timeout. If the animation loops and the
    * timeout falls in between a certain range then the timeout is adjusted so
    * that it's never 0. If the animation does not loop then no adjustments are
    * made.
    */
-  int32_t GetTimeoutForFrame(uint32_t aFrameNum) const;
-
-  /*
-   * Set number of times to loop the image.
-   * @note -1 means loop forever.
-   */
-  void SetLoopCount(int32_t aLoopCount) { mLoopCount = aLoopCount; }
-  int32_t LoopCount() const { return mLoopCount; }
-
-  /*
-   * Set the timeout for the first frame. This is used to allow animation
-   * scheduling even before a full decode runs for this image.
-   */
-  void SetFirstFrameTimeout(int32_t aTimeout) { mFirstFrameTimeout = aTimeout; }
+  int32_t GetTimeoutForFrame(AnimationState& aState, uint32_t aFrameNum) const;
 
   /**
    * Collect an accounting of the memory occupied by the compositing surfaces we
    * use during animation playback. All of the actual animation frames are
    * stored in the SurfaceCache, so we don't need to report them here.
    */
   void CollectSizeOfCompositingSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                         MallocSizeOf aMallocSizeOf) 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 -1. Can return 0 in the case of an animated
-   * image that has a 0ms delay between its frames and does not loop.
-   */
-  int32_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(TimeStamp aTime);
+  RefreshResult AdvanceFrame(AnimationState& aState, TimeStamp aTime);
+
+  /**
+   * Get the @aIndex-th frame in the frame index, ignoring results of blending.
+   */
+  RawAccessFrameRef GetRawFrame(uint32_t aFrameNum) const;
+
+  /**
+   * 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 -1. Can return 0 in the case of an animated
+   * image that has a 0ms delay between its frames and does not loop.
+   */
+  int32_t GetSingleLoopTime(AnimationState& aState) const;
 
   /**
    * Get the time the frame we're currently displaying is supposed to end.
    *
    * In the error case, returns an "infinity" timestamp.
    */
-  TimeStamp GetCurrentImgFrameEndTime() const;
-
-  bool DoBlend(nsIntRect* aDirtyRect, uint32_t aPrevFrameIndex,
-               uint32_t aNextFrameIndex);
+  TimeStamp GetCurrentImgFrameEndTime(AnimationState& aState) const;
 
-  /**
-   * Get the @aIndex-th frame in the frame index, ignoring results of blending.
-   */
-  RawAccessFrameRef GetRawFrame(uint32_t aFrameNum) const;
+  bool DoBlend(AnimationState& aState,
+               nsIntRect* aDirtyRect,
+               uint32_t aPrevFrameIndex,
+               uint32_t aNextFrameIndex);
 
   /** Clears an area of <aFrame> with transparent black.
    *
    * @param aFrameData Target Frame data
    * @param aFrameRect The rectangle of the data pointed ot by aFrameData
    *
    * @note Does also clears the transparency mask
    */
@@ -269,41 +306,14 @@ private: // data
 
   /** the previous composited frame, for DISPOSE_RESTORE_PREVIOUS
    *
    * The Previous Frame (all frames composited up to the current) needs to be
    * stored in cases where the image specifies it wants the last frame back
    * when it's done with the current frame.
    */
   RawAccessFrameRef mCompositingPrevFrame;
-
-  //! 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 mLoopRemainingCount;
-
-  //! Track the last composited frame for Optimizations (See DoComposite code)
-  int32_t mLastCompositedFrameIndex;
-
-  //! The total number of loops for the image.
-  int32_t mLoopCount;
-
-  //! The timeout for the first frame of this image.
-  int32_t mFirstFrameTimeout;
-
-  //! 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_image_FrameAnimator_h
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -172,18 +172,19 @@ RasterImage::RequestRefresh(const TimeSt
 
   EvaluateAnimation();
 
   if (!mAnimating) {
     return;
   }
 
   FrameAnimator::RefreshResult res;
-  if (mAnim) {
-    res = mAnim->RequestRefresh(aTime);
+  if (mAnimationState) {
+    MOZ_ASSERT(mFrameAnimator);
+    res = mFrameAnimator->RequestRefresh(*mAnimationState, aTime);
   }
 
   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++;
@@ -269,26 +270,27 @@ RasterImage::GetType(uint16_t* aType)
   return NS_OK;
 }
 
 LookupResult
 RasterImage::LookupFrameInternal(uint32_t aFrameNum,
                                  const IntSize& aSize,
                                  uint32_t aFlags)
 {
-  if (!mAnim) {
+  if (!mAnimationState) {
     NS_ASSERTION(aFrameNum == 0,
                  "Don't ask for a frame > 0 if we're not animated!");
     aFrameNum = 0;
   }
 
-  if (mAnim && aFrameNum > 0) {
+  if (mAnimationState && aFrameNum > 0) {
+    MOZ_ASSERT(mFrameAnimator);
     MOZ_ASSERT(ToSurfaceFlags(aFlags) == DefaultSurfaceFlags(),
                "Can't composite frames with non-default surface flags");
-    return mAnim->GetCompositedFrame(aFrameNum);
+    return mFrameAnimator->GetCompositedFrame(*mAnimationState, aFrameNum);
   }
 
   SurfaceFlags surfaceFlags = ToSurfaceFlags(aFlags);
 
   // We don't want any substitution for sync decodes, and substitution would be
   // illegal when high quality downscaling is disabled, so we use
   // SurfaceCache::Lookup in this case.
   if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) {
@@ -332,17 +334,18 @@ RasterImage::LookupFrame(uint32_t aFrame
   }
 
   if (result.Type() == MatchType::NOT_FOUND ||
       result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
       ((aFlags & FLAG_SYNC_DECODE) && !result)) {
     // We don't have a copy of this frame, and there's no decoder working on
     // one. (Or we're sync decoding and the existing decoder hasn't even started
     // yet.) Trigger decoding so it'll be available next time.
-    MOZ_ASSERT(!mAnim || GetNumFrames() < 1, "Animated frames should be locked");
+    MOZ_ASSERT(!mAnimationState || GetNumFrames() < 1,
+               "Animated frames should be locked");
 
     Decode(requestedSize, aFlags);
 
     // If we can sync decode, we should already have the frame.
     if (aFlags & FLAG_SYNC_DECODE) {
       result = LookupFrameInternal(aFrameNum, requestedSize, aFlags);
     }
   }
@@ -376,34 +379,34 @@ RasterImage::LookupFrame(uint32_t aFrame
   }
 
   return Move(result.DrawableRef());
 }
 
 uint32_t
 RasterImage::GetCurrentFrameIndex() const
 {
-  if (mAnim) {
-    return mAnim->GetCurrentAnimationFrameIndex();
+  if (mAnimationState) {
+    return mAnimationState->GetCurrentAnimationFrameIndex();
   }
 
   return 0;
 }
 
 uint32_t
 RasterImage::GetRequestedFrameIndex(uint32_t aWhichFrame) const
 {
   return aWhichFrame == FRAME_FIRST ? 0 : GetCurrentFrameIndex();
 }
 
 IntRect
 RasterImage::GetFirstFrameRect()
 {
-  if (mAnim && mHasBeenDecoded) {
-    return mAnim->GetFirstFrameRefreshArea();
+  if (mAnimationState && mHasBeenDecoded) {
+    return mAnimationState->GetFirstFrameRefreshArea();
   }
 
   // Fall back to our size. This is implicitly zero-size if !mHasSize.
   return IntRect(IntPoint(0,0), mSize);
 }
 
 NS_IMETHODIMP_(bool)
 RasterImage::IsOpaque()
@@ -436,27 +439,27 @@ NS_IMETHODIMP
 RasterImage::GetAnimated(bool* aAnimated)
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   NS_ENSURE_ARG_POINTER(aAnimated);
 
-  // If we have mAnim, we can know for sure
-  if (mAnim) {
+  // If we have an AnimationState, we can know for sure.
+  if (mAnimationState) {
     *aAnimated = true;
     return NS_OK;
   }
 
   // Otherwise, we need to have been decoded to know for sure, since if we were
-  // decoded at least once mAnim would have been created for animated images.
-  // This is true even though we check for animation during the metadata decode,
-  // because we may still discover animation only during the full decode for
-  // corrupt images.
+  // decoded at least once mAnimationState would have been created for animated
+  // images. This is true even though we check for animation during the
+  // metadata decode, because we may still discover animation only during the
+  // full decode for corrupt images.
   if (!mHasBeenDecoded) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // We know for sure
   *aAnimated = false;
 
   return NS_OK;
@@ -470,18 +473,19 @@ RasterImage::GetFirstFrameDelay()
     return -1;
   }
 
   bool animated = false;
   if (NS_FAILED(GetAnimated(&animated)) || !animated) {
     return -1;
   }
 
-  MOZ_ASSERT(mAnim, "Animated images should have a FrameAnimator");
-  return mAnim->GetTimeoutForFrame(0);
+  MOZ_ASSERT(mAnimationState, "Animated images should have an AnimationState");
+  MOZ_ASSERT(mFrameAnimator, "Animated images should have a FrameAnimator");
+  return mFrameAnimator->GetTimeoutForFrame(*mAnimationState, 0);
 }
 
 already_AddRefed<SourceSurface>
 RasterImage::CopyFrame(uint32_t aWhichFrame, uint32_t aFlags)
 {
   if (aWhichFrame > FRAME_MAX_VALUE) {
     return nullptr;
   }
@@ -746,18 +750,18 @@ RasterImage::SizeOfSourceWithComputedFal
   return mSourceBuffer->SizeOfIncludingThisWithComputedFallback(aMallocSizeOf);
 }
 
 void
 RasterImage::CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                    MallocSizeOf aMallocSizeOf) const
 {
   SurfaceCache::CollectSizeOfSurfaces(ImageKey(this), aCounters, aMallocSizeOf);
-  if (mAnim) {
-    mAnim->CollectSizeOfCompositingSurfaces(aCounters, aMallocSizeOf);
+  if (mFrameAnimator) {
+    mFrameAnimator->CollectSizeOfCompositingSurfaces(aCounters, aMallocSizeOf);
   }
 }
 
 class OnAddedFrameRunnable : public Runnable
 {
 public:
   OnAddedFrameRunnable(RasterImage* aImage,
                        uint32_t aNewFrameCount,
@@ -797,25 +801,25 @@ RasterImage::OnAddedFrame(uint32_t aNewF
   if (mError) {
     return;  // We're in an error state, possibly due to OOM. Bail.
   }
 
   if (aNewFrameCount > mFrameCount) {
     mFrameCount = aNewFrameCount;
 
     if (aNewFrameCount == 2) {
-      MOZ_ASSERT(mAnim, "Should already have animation state");
+      MOZ_ASSERT(mAnimationState, "Should already have animation state");
 
       // We may be able to start animating.
       if (mPendingAnimation && ShouldAnimate()) {
         StartAnimation();
       }
     }
     if (aNewFrameCount > 1) {
-      mAnim->UnionFirstFrameRefreshArea(aNewRefreshArea);
+      mAnimationState->UnionFirstFrameRefreshArea(aNewRefreshArea);
     }
   }
 }
 
 bool
 RasterImage::SetMetadata(const ImageMetadata& aMetadata,
                          bool aFromMetadataDecode)
 {
@@ -845,36 +849,37 @@ RasterImage::SetMetadata(const ImageMeta
     }
 
     // Set the size and flag that we have it.
     mSize = size;
     mOrientation = orientation;
     mHasSize = true;
   }
 
-  if (mHasSize && aMetadata.HasAnimation() && !mAnim) {
+  if (mHasSize && aMetadata.HasAnimation() && !mAnimationState) {
     // We're becoming animated, so initialize animation stuff.
-    mAnim = MakeUnique<FrameAnimator>(this, mSize, mAnimationMode);
+    mAnimationState.emplace(mAnimationMode);
+    mFrameAnimator = MakeUnique<FrameAnimator>(this, mSize);
 
     // We don't support discarding animated images (See bug 414259).
     // Lock the image and throw away the key.
     LockImage();
 
     if (!aFromMetadataDecode) {
       // The metadata decode reported that this image isn't animated, but we
       // discovered that it actually was during the full decode. This is a
       // rare failure that only occurs for corrupt images. To recover, we need
       // to discard all existing surfaces and redecode.
       return false;
     }
   }
 
-  if (mAnim) {
-    mAnim->SetLoopCount(aMetadata.GetLoopCount());
-    mAnim->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout());
+  if (mAnimationState) {
+    mAnimationState->SetLoopCount(aMetadata.GetLoopCount());
+    mAnimationState->SetFirstFrameTimeout(aMetadata.GetFirstFrameTimeout());
   }
 
   if (aMetadata.HasHotspot()) {
     IntPoint hotspot = aMetadata.GetHotspot();
 
     nsCOMPtr<nsISupportsPRUint32> intwrapx =
       do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID);
     nsCOMPtr<nsISupportsPRUint32> intwrapy =
@@ -887,18 +892,18 @@ RasterImage::SetMetadata(const ImageMeta
   }
 
   return true;
 }
 
 NS_IMETHODIMP
 RasterImage::SetAnimationMode(uint16_t aAnimationMode)
 {
-  if (mAnim) {
-    mAnim->SetAnimationMode(aAnimationMode);
+  if (mAnimationState) {
+    mAnimationState->SetAnimationMode(aAnimationMode);
   }
   return SetAnimationModeInternal(aAnimationMode);
 }
 
 //******************************************************************************
 
 nsresult
 RasterImage::StartAnimation()
@@ -906,102 +911,107 @@ RasterImage::StartAnimation()
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   MOZ_ASSERT(ShouldAnimate(), "Should not animate!");
 
   // If we're not ready to animate, then set mPendingAnimation, which will cause
   // us to start animating if and when we do become ready.
-  mPendingAnimation = !mAnim || GetNumFrames() < 2;
+  mPendingAnimation = !mAnimationState || GetNumFrames() < 2;
   if (mPendingAnimation) {
     return NS_OK;
   }
 
+  MOZ_ASSERT(mFrameAnimator,
+             "Should have a FrameAnimator if we have AnimationState");
+
   // A timeout of -1 means we should display this frame forever.
-  if (mAnim->GetTimeoutForFrame(GetCurrentFrameIndex()) < 0) {
+  if (mFrameAnimator->GetTimeoutForFrame(*mAnimationState,
+                                         GetCurrentFrameIndex())
+        < 0) {
     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().
-  mAnim->InitAnimationFrameTimeIfNecessary();
+  mAnimationState->InitAnimationFrameTimeIfNecessary();
 
   return NS_OK;
 }
 
 //******************************************************************************
 nsresult
 RasterImage::StopAnimation()
 {
   MOZ_ASSERT(mAnimating, "Should be animating!");
 
   nsresult rv = NS_OK;
   if (mError) {
     rv = NS_ERROR_FAILURE;
   } else {
-    mAnim->SetAnimationFrameTime(TimeStamp());
+    mAnimationState->SetAnimationFrameTime(TimeStamp());
   }
 
   mAnimating = false;
   return rv;
 }
 
 //******************************************************************************
 NS_IMETHODIMP
 RasterImage::ResetAnimation()
 {
   if (mError) {
     return NS_ERROR_FAILURE;
     }
 
   mPendingAnimation = false;
 
-  if (mAnimationMode == kDontAnimMode || !mAnim ||
-      mAnim->GetCurrentAnimationFrameIndex() == 0) {
+  if (mAnimationMode == kDontAnimMode || !mAnimationState ||
+      mAnimationState->GetCurrentAnimationFrameIndex() == 0) {
     return NS_OK;
   }
 
   mAnimationFinished = false;
 
   if (mAnimating) {
     StopAnimation();
   }
 
-  MOZ_ASSERT(mAnim, "Should have a FrameAnimator");
-  mAnim->ResetAnimation();
+  MOZ_ASSERT(mAnimationState, "Should have AnimationState");
+  mAnimationState->ResetAnimation();
 
-  NotifyProgress(NoProgress, mAnim->GetFirstFrameRefreshArea());
+  NotifyProgress(NoProgress, mAnimationState->GetFirstFrameRefreshArea());
 
   // Start the animation again. It may not have been running before, if
   // mAnimationFinished was true before entering this function.
   EvaluateAnimation();
 
   return NS_OK;
 }
 
 //******************************************************************************
 NS_IMETHODIMP_(void)
 RasterImage::SetAnimationStartTime(const TimeStamp& aTime)
 {
-  if (mError || mAnimationMode == kDontAnimMode || mAnimating || !mAnim) {
+  if (mError || mAnimationMode == kDontAnimMode || mAnimating || !mAnimationState) {
     return;
   }
 
-  mAnim->SetAnimationFrameTime(aTime);
+  mAnimationState->SetAnimationFrameTime(aTime);
 }
 
 NS_IMETHODIMP_(float)
 RasterImage::GetFrameIndex(uint32_t aWhichFrame)
 {
   MOZ_ASSERT(aWhichFrame <= FRAME_MAX_VALUE, "Invalid argument");
-  return (aWhichFrame == FRAME_FIRST || !mAnim)
+  return (aWhichFrame == FRAME_FIRST || !mAnimationState)
          ? 0.0f
-         : mAnim->GetCurrentAnimationFrameIndex();
+         : mAnimationState->GetCurrentAnimationFrameIndex();
 }
 
 NS_IMETHODIMP_(IntRect)
 RasterImage::GetImageSpaceInvalidationRect(const IntRect& aRect)
 {
   return aRect;
 }
 
@@ -1152,31 +1162,31 @@ RasterImage::GetKeys(uint32_t* count, ch
   return mProperties->GetKeys(count, keys);
 }
 
 void
 RasterImage::Discard()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(CanDiscard(), "Asked to discard but can't");
-  MOZ_ASSERT(!mAnim, "Asked to discard for animated image");
+  MOZ_ASSERT(!mAnimationState, "Asked to discard for animated image");
 
   // Delete all the decoded frames.
   SurfaceCache::RemoveImage(ImageKey(this));
 
   // Notify that we discarded.
   if (mProgressTracker) {
     mProgressTracker->OnDiscard();
   }
 }
 
 bool
 RasterImage::CanDiscard() {
   return mHasSourceData &&       // ...have the source data...
-         !mAnim;                 // Can never discard animated images
+         !mAnimationState;       // Can never discard animated images
 }
 
 NS_IMETHODIMP
 RasterImage::StartDecoding()
 {
   if (mError) {
     return NS_ERROR_FAILURE;
   }
@@ -1291,17 +1301,17 @@ RasterImage::Decode(const IntSize& aSize
   if (IsOpaque()) {
     // If there's no transparency, it doesn't matter whether we premultiply
     // alpha or not.
     surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA;
   }
 
   // Create a decoder.
   RefPtr<IDecodingTask> task;
-  if (mAnim) {
+  if (mAnimationState) {
     task = DecoderFactory::CreateAnimationDecoder(mDecoderType, WrapNotNull(this),
                                                   mSourceBuffer, mSize,
                                                   decoderFlags, surfaceFlags);
   } else {
     task = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this),
                                          mSourceBuffer, mSize, targetSize,
                                          decoderFlags, surfaceFlags,
                                          mRequestedSampleSize);
@@ -1357,17 +1367,17 @@ RasterImage::RecoverFromInvalidFrames(co
 
   // Relock the image if it's supposed to be locked.
   if (mLockCount > 0) {
     SurfaceCache::LockImage(ImageKey(this));
   }
 
   // Animated images require some special handling, because we normally require
   // that they never be discarded.
-  if (mAnim) {
+  if (mAnimationState) {
     Decode(mSize, aFlags | FLAG_SYNC_DECODE);
     ResetAnimation();
     return;
   }
 
   // For non-animated images, it's fine to recover using an async decode.
   Decode(aSize, aFlags);
 }
@@ -1390,17 +1400,17 @@ RasterImage::CanDownscaleDuringDecode(co
   // our size, and the flags allow us to do it.
   if (!mHasSize || mTransient || !HaveSkia() ||
       !gfxPrefs::ImageDownscaleDuringDecodeEnabled() ||
       !(aFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING)) {
     return false;
   }
 
   // We don't downscale animated images during decode.
-  if (mAnim) {
+  if (mAnimationState) {
     return false;
   }
 
   // Never upscale.
   if (aSize.width >= mSize.width || aSize.height >= mSize.height) {
     return false;
   }
 
@@ -1603,17 +1613,18 @@ RasterImage::DoError()
 
   // Put the container in an error state.
   mError = true;
 
   // Stop animation and release our FrameAnimator.
   if (mAnimating) {
     StopAnimation();
   }
-  mAnim = nullptr;
+  mAnimationState = Nothing();
+  mFrameAnimator = nullptr;
 
   // Release all locks.
   mLockCount = 0;
   SurfaceCache::UnlockImage(ImageKey(this));
 
   // Release all frames from the surface cache.
   SurfaceCache::RemoveImage(ImageKey(this));
 
@@ -1716,18 +1727,18 @@ RasterImage::FinalizeDecoder(Decoder* aD
   }
 
   MOZ_ASSERT(mError || mHasSize || !aDecoder->HasSize(),
              "SetMetadata should've gotten a size");
 
   if (!wasMetadata && aDecoder->GetDecodeDone() && !aDecoder->WasAborted()) {
     // Flag that we've been decoded before.
     mHasBeenDecoded = true;
-    if (mAnim) {
-      mAnim->SetDoneDecoding(true);
+    if (mAnimationState) {
+      mAnimationState->SetDoneDecoding(true);
     }
   }
 
   // Send out any final notifications.
   NotifyProgress(aDecoder->TakeProgress(),
                  aDecoder->TakeInvalidRect(),
                  aDecoder->GetSurfaceFlags());
 
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -22,16 +22,17 @@
 #include "imgIContainer.h"
 #include "nsIProperties.h"
 #include "nsTArray.h"
 #include "imgFrame.h"
 #include "LookupResult.h"
 #include "nsThreadUtils.h"
 #include "DecodePool.h"
 #include "DecoderFactory.h"
+#include "FrameAnimator.h"
 #include "Orientation.h"
 #include "nsIObserver.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/Pair.h"
 #include "mozilla/TimeStamp.h"
@@ -128,17 +129,16 @@ namespace mozilla {
 namespace layers {
 class ImageContainer;
 class Image;
 } // namespace layers
 
 namespace image {
 
 class Decoder;
-class FrameAnimator;
 class ImageMetadata;
 class SourceBuffer;
 
 class RasterImage final : public ImageResource
                         , public nsIProperties
                         , public SupportsWeakPtr<RasterImage>
 #ifdef DEBUG
                         , public imgIContainerDebug
@@ -282,17 +282,17 @@ private:
 
   void UpdateImageContainer();
 
   // We would like to just check if we have a zero lock count, but we can't do
   // that for animated images because in EnsureAnimExists we lock the image and
   // never unlock so that animated images always have their lock count >= 1. In
   // that case we use our animation consumers count as a proxy for lock count.
   bool IsUnlocked() {
-    return (mLockCount == 0 || (mAnim && mAnimationConsumers == 0));
+    return (mLockCount == 0 || (mAnimationState && mAnimationConsumers == 0));
   }
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Decoding.
   //////////////////////////////////////////////////////////////////////////////
 
   /**
@@ -345,17 +345,20 @@ private: // data
   Orientation                mOrientation;
 
   /// If this has a value, we're waiting for SetSize() to send the load event.
   Maybe<Progress>            mLoadProgress;
 
   nsCOMPtr<nsIProperties>   mProperties;
 
   /// If this image is animated, a FrameAnimator which manages its animation.
-  UniquePtr<FrameAnimator> mAnim;
+  UniquePtr<FrameAnimator> mFrameAnimator;
+
+  /// Animation timeline and other state for animation images.
+  Maybe<AnimationState> mAnimationState;
 
   // Image locking.
   uint32_t                   mLockCount;
 
   // The type of decoder this image needs. Computed from the MIME type in Init().
   DecoderType                mDecoderType;
 
   // How many times we've decoded this image.