Bug 1289957 (Part 2) - Notify RasterImage about new frames in NotifyProgress() and remove OnAddedFrame(). r=edwin
authorSeth Fowler <mark.seth.fowler@gmail.com>
Wed, 27 Jul 2016 17:12:25 -0700
changeset 307004 70369395b75433f29f313b7426681c2bd7ec4bc1
parent 307003 8c4fb79a81438d8a8242c2e5c564a0fadc4494cf
child 307005 996b4f2f996350d358d6ecb629e81ed0c4eff9ea
push id30502
push usercbook@mozilla.com
push dateThu, 28 Jul 2016 15:43:16 +0000
treeherdermozilla-central@9ec789c0ee5b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedwin
bugs1289957
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 1289957 (Part 2) - Notify RasterImage about new frames in NotifyProgress() and remove OnAddedFrame(). r=edwin
image/Decoder.cpp
image/Decoder.h
image/FrameAnimator.cpp
image/FrameAnimator.h
image/IDecodingTask.cpp
image/RasterImage.cpp
image/RasterImage.h
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -53,16 +53,17 @@ Decoder::Decoder(RasterImage* aImage)
   , mProgress(NoProgress)
   , mFrameCount(0)
   , mLoopLength(FrameTimeout::Zero())
   , mDecoderFlags(DefaultDecoderFlags())
   , mSurfaceFlags(DefaultSurfaceFlags())
   , mInitialized(false)
   , mMetadataDecode(false)
   , mInFrame(false)
+  , mFinishedNewFrame(false)
   , mReachedTerminalState(false)
   , mDecodeDone(false)
   , mError(false)
   , mDecodeAborted(false)
   , mShouldReportError(false)
 { }
 
 Decoder::~Decoder()
@@ -241,16 +242,24 @@ Decoder::SetTargetSize(const nsIntSize& 
 }
 
 Maybe<IntSize>
 Decoder::GetTargetSize()
 {
   return mDownscaler ? Some(mDownscaler->TargetSize()) : Nothing();
 }
 
+Maybe<uint32_t>
+Decoder::TakeCompleteFrameCount()
+{
+  const bool finishedNewFrame = mFinishedNewFrame;
+  mFinishedNewFrame = false;
+  return finishedNewFrame ? Some(GetCompleteFrameCount()) : Nothing();
+}
+
 nsresult
 Decoder::AllocateFrame(uint32_t aFrameNum,
                        const nsIntSize& aTargetSize,
                        const nsIntRect& aFrameRect,
                        gfx::SurfaceFormat aFormat,
                        uint8_t aPaletteDepth)
 {
   mCurrentFrame = AllocateFrameInternal(aFrameNum, aTargetSize, aFrameRect,
@@ -362,20 +371,16 @@ Decoder::AllocateFrameInternal(uint32_t 
 
     // 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.
     mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, frame->GetRect());
   }
 
   mFrameCount++;
 
-  if (mImage) {
-    mImage->OnAddedFrame(mFrameCount);
-  }
-
   return ref;
 }
 
 /*
  * Hook stubs. Override these as necessary in decoder implementations.
  */
 
 nsresult Decoder::InitInternal() { return NS_OK; }
@@ -426,18 +431,19 @@ Decoder::PostFrameStop(Opacity aFrameOpa
                        BlendMethod aBlendMethod /* = BlendMethod::OVER */,
                        const Maybe<nsIntRect>& aBlendRect /* = Nothing() */)
 {
   // We should be mid-frame
   MOZ_ASSERT(!IsMetadataDecode(), "Stopping frame during metadata decode");
   MOZ_ASSERT(mInFrame, "Stopping frame when we didn't start one");
   MOZ_ASSERT(mCurrentFrame, "Stopping frame when we don't have one");
 
-  // Update our state
+  // Update our state.
   mInFrame = false;
+  mFinishedNewFrame = true;
 
   mCurrentFrame->Finish(aFrameOpacity, aDisposalMethod, aTimeout,
                         aBlendMethod, aBlendRect);
 
   mProgress |= FLAG_FRAME_COMPLETE;
 
   mLoopLength += aTimeout;
 
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -88,17 +88,17 @@ public:
     return progress;
   }
 
   /**
    * Returns true if there's any progress to report.
    */
   bool HasProgress() const
   {
-    return mProgress != NoProgress || !mInvalidRect.IsEmpty();
+    return mProgress != NoProgress || !mInvalidRect.IsEmpty() || mFinishedNewFrame;
   }
 
   /*
    * State.
    */
 
   /**
    * If we're doing a metadata decode, we only decode the image's headers, which
@@ -180,26 +180,27 @@ public:
 
   // The number of chunks this decoder's input was divided into.
   uint32_t ChunkCount() const
   {
     MOZ_ASSERT(mIterator);
     return mIterator->ChunkCount();
   }
 
+  /**
+   * @return the number of complete animation frames which have been decoded so
+   * far, if it has changed since the last call to TakeCompleteFrameCount();
+   * otherwise, returns Nothing().
+   */
+  Maybe<uint32_t> TakeCompleteFrameCount();
+
   // The number of frames we have, including anything in-progress. Thus, this
   // is only 0 if we haven't begun any frames.
   uint32_t GetFrameCount() { return mFrameCount; }
 
-  // The number of complete frames we have (ie, not including anything
-  // in-progress).
-  uint32_t GetCompleteFrameCount() {
-    return mInFrame ? mFrameCount - 1 : mFrameCount;
-  }
-
   // Did we discover that the image we're decoding is animated?
   bool HasAnimation() const { return mImageMetadata.HasAnimation(); }
 
   // Error tracking
   bool HasError() const { return mError; }
   bool ShouldReportError() const { return mShouldReportError; }
 
   /// Did we finish decoding enough that calling Decode() again would be useless?
@@ -398,16 +399,27 @@ private:
 
   /**
    * CompleteDecode() finishes up the decoding process after Decode() determines
    * that we're finished. It records final progress and does all the cleanup
    * that's possible off-main-thread.
    */
   void CompleteDecode();
 
+  /// @return the number of complete frames we have. Does not include the
+  /// current frame if it's unfinished.
+  uint32_t GetCompleteFrameCount()
+  {
+    if (mFrameCount == 0) {
+      return 0;
+    }
+
+    return mInFrame ? mFrameCount - 1 : mFrameCount;
+  }
+
   RawAccessFrameRef AllocateFrameInternal(uint32_t aFrameNum,
                                           const nsIntSize& aTargetSize,
                                           const nsIntRect& aFrameRect,
                                           gfx::SurfaceFormat aFormat,
                                           uint8_t aPaletteDepth,
                                           imgFrame* aPreviousFrame);
 
 protected:
@@ -435,16 +447,18 @@ private:
   TimeDuration mDecodeTime;
 
   DecoderFlags mDecoderFlags;
   SurfaceFlags mSurfaceFlags;
 
   bool mInitialized : 1;
   bool mMetadataDecode : 1;
   bool mInFrame : 1;
+  bool mFinishedNewFrame : 1;  // True if PostFrameStop() has been called since
+                               // the last call to TakeCompleteFrameCount().
   bool mReachedTerminalState : 1;
   bool mDecodeDone : 1;
   bool mError : 1;
   bool mDecodeAborted : 1;
   bool mShouldReportError : 1;
 };
 
 } // namespace image
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -39,16 +39,38 @@ AnimationState::ResetAnimation()
 
 void
 AnimationState::SetAnimationMode(uint16_t aAnimationMode)
 {
   mAnimationMode = aAnimationMode;
 }
 
 void
+AnimationState::UpdateKnownFrameCount(uint32_t aFrameCount)
+{
+  if (aFrameCount <= mFrameCount) {
+    // Nothing to do. Since we can redecode animated images, we may see the same
+    // sequence of updates replayed again, so seeing a smaller frame count than
+    // what we already know about doesn't indicate an error.
+    return;
+  }
+
+  MOZ_ASSERT(!mDoneDecoding, "Adding new frames after decoding is finished?");
+  MOZ_ASSERT(aFrameCount <= mFrameCount + 1, "Skipped a frame?");
+
+  mFrameCount = aFrameCount;
+}
+
+Maybe<uint32_t>
+AnimationState::FrameCount() const
+{
+  return mDoneDecoding ? Some(mFrameCount) : Nothing();
+}
+
+void
 AnimationState::SetFirstFrameRefreshArea(const IntRect& aRefreshArea)
 {
   mFirstFrameRefreshArea = aRefreshArea;
 }
 
 void
 AnimationState::InitAnimationFrameTimeIfNecessary()
 {
@@ -124,36 +146,19 @@ FrameAnimator::AdvanceFrame(AnimationSta
   PROFILER_LABEL_FUNC(js::ProfileEntry::Category::GRAPHICS);
 
   RefreshResult ret;
 
   // Determine what the next frame is, taking into account looping.
   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 (!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.
-      aState.mCurrentAnimationFrameTime = aTime;
-      return ret;
-    }
-
-    // End of an animation loop...
-
+  // Check if we're at the end of the loop. (FrameCount() returns Nothing() if
+  // we don't know the total count yet.)
+  if (aState.FrameCount() == Some(nextFrameIndex)) {
     // If we are not looping forever, initialize the loop counter
     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 (aState.mAnimationMode == imgIContainer::kLoopOnceAnimMode ||
@@ -168,22 +173,37 @@ FrameAnimator::AdvanceFrame(AnimationSta
     }
 
     // If we're done, exit early.
     if (ret.mAnimationFinished) {
       return ret;
     }
   }
 
-  // There can be frames in the surface cache with index >= mImage->GetNumFrames()
-  // that GetRawFrame can access because the decoding thread has decoded them, but
-  // RasterImage hasn't acknowledged those frames yet. We don't want to go past
-  // 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());
+  if (nextFrameIndex >= aState.KnownFrameCount()) {
+    // 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.
+    aState.mCurrentAnimationFrameTime = aTime;
+    return ret;
+  }
+
+  // There can be frames in the surface cache with index >= KnownFrameCount()
+  // which GetRawFrame() can access because an async decoder has decoded them,
+  // but which AnimationState doesn't know about yet because we haven't received
+  // the appropriate notification on the main thread. Make sure we stay in sync
+  // with AnimationState.
+  MOZ_ASSERT(nextFrameIndex < aState.KnownFrameCount());
   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 = aState.mDoneDecoding ||
                     (nextFrame && nextFrame->IsFinished());
 
--- a/image/FrameAnimator.h
+++ b/image/FrameAnimator.h
@@ -20,17 +20,18 @@ namespace mozilla {
 namespace image {
 
 class RasterImage;
 
 class AnimationState
 {
 public:
   explicit AnimationState(uint16_t aAnimationMode)
-    : mCurrentAnimationFrameIndex(0)
+    : mFrameCount(0)
+    , mCurrentAnimationFrameIndex(0)
     , mLoopRemainingCount(-1)
     , mLoopCount(-1)
     , mFirstFrameTimeout(FrameTimeout::FromRawMilliseconds(0))
     , mAnimationMode(aAnimationMode)
     , mDoneDecoding(false)
   { }
 
   /**
@@ -47,16 +48,26 @@ public:
 
   /**
    * The animation mode of the image.
    *
    * Constants defined in imgIContainer.idl.
    */
   void SetAnimationMode(uint16_t aAnimationMode);
 
+  /// Update the number of frames of animation this image is known to have.
+  void UpdateKnownFrameCount(uint32_t aFrameCount);
+
+  /// @return the number of frames of animation we know about so far.
+  uint32_t KnownFrameCount() const { return mFrameCount; }
+
+  /// @return the number of frames this animation has, if we know for sure.
+  /// (In other words, if decoding is finished.) Otherwise, returns Nothing().
+  Maybe<uint32_t> FrameCount() const;
+
   /**
    * Get or set the area of the image to invalidate when we loop around to the
    * first frame.
    */
   void SetFirstFrameRefreshArea(const gfx::IntRect& aRefreshArea);
   gfx::IntRect FirstFrameRefreshArea() const { return mFirstFrameRefreshArea; }
 
   /**
@@ -103,17 +114,20 @@ private:
   friend class FrameAnimator;
 
   //! Area of the first frame that needs to be redrawn on subsequent loops.
   gfx::IntRect mFirstFrameRefreshArea;
 
   //! the time that the animation advanced to the current frame
   TimeStamp mCurrentAnimationFrameTime;
 
-  //! The current frame index we're on. 0 to (numFrames - 1).
+  //! The number of frames of animation this image has.
+  uint32_t mFrameCount;
+
+  //! The current frame index we're on, in the range [0, mFrameCount).
   uint32_t mCurrentAnimationFrameIndex;
 
   //! number of loops remaining before animation stops (-1 no stop)
   int32_t mLoopRemainingCount;
 
   //! The total number of loops for the image.
   int32_t mLoopCount;
 
--- a/image/IDecodingTask.cpp
+++ b/image/IDecodingTask.cpp
@@ -20,30 +20,33 @@ namespace image {
 ///////////////////////////////////////////////////////////////////////////////
 
 static void
 NotifyProgress(NotNull<Decoder*> aDecoder)
 {
   MOZ_ASSERT(aDecoder->HasProgress() && !aDecoder->IsMetadataDecode());
 
   Progress progress = aDecoder->TakeProgress();
-  nsIntRect invalidRect = aDecoder->TakeInvalidRect();
+  IntRect invalidRect = aDecoder->TakeInvalidRect();
+  Maybe<uint32_t> frameCount = aDecoder->TakeCompleteFrameCount();
   SurfaceFlags surfaceFlags = aDecoder->GetSurfaceFlags();
 
   // Synchronously notify if we can.
   if (NS_IsMainThread() &&
       !(aDecoder->GetDecoderFlags() & DecoderFlags::ASYNC_NOTIFY)) {
-    aDecoder->GetImage()->NotifyProgress(progress, invalidRect, surfaceFlags);
+    aDecoder->GetImage()->NotifyProgress(progress, invalidRect,
+                                         frameCount, surfaceFlags);
     return;
   }
 
   // We're forced to notify asynchronously.
   NotNull<RefPtr<Decoder>> decoder = aDecoder;
   NS_DispatchToMainThread(NS_NewRunnableFunction([=]() -> void {
-    decoder->GetImage()->NotifyProgress(progress, invalidRect, surfaceFlags);
+    decoder->GetImage()->NotifyProgress(progress, invalidRect,
+                                        frameCount, surfaceFlags);
   }));
 }
 
 static void
 NotifyDecodeComplete(NotNull<Decoder*> aDecoder)
 {
   // Synchronously notify if we can.
   if (NS_IsMainThread() &&
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -76,17 +76,16 @@ RasterImage::RasterImage(ImageURL* aURI 
   mRequestedSampleSize(0),
   mImageProducerID(ImageContainer::AllocateProducerID()),
   mLastFrameID(0),
   mLastImageContainerDrawResult(DrawResult::NOT_READY),
 #ifdef DEBUG
   mFramesNotified(0),
 #endif
   mSourceBuffer(WrapNotNull(new SourceBuffer())),
-  mFrameCount(0),
   mHasSize(false),
   mTransient(false),
   mSyncLoad(false),
   mDiscardable(false),
   mHasSourceData(false),
   mHasBeenDecoded(false),
   mPendingAnimation(false),
   mAnimationFinished(false),
@@ -334,17 +333,17 @@ 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(!mAnimationState || GetNumFrames() < 1,
+    MOZ_ASSERT(!mAnimationState || mAnimationState->KnownFrameCount() < 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);
     }
@@ -754,67 +753,16 @@ RasterImage::CollectSizeOfSurfaces(nsTAr
                                    MallocSizeOf aMallocSizeOf) const
 {
   SurfaceCache::CollectSizeOfSurfaces(ImageKey(this), aCounters, aMallocSizeOf);
   if (mFrameAnimator) {
     mFrameAnimator->CollectSizeOfCompositingSurfaces(aCounters, aMallocSizeOf);
   }
 }
 
-class OnAddedFrameRunnable : public Runnable
-{
-public:
-  OnAddedFrameRunnable(RasterImage* aImage, uint32_t aNewFrameCount)
-    : mImage(aImage)
-    , mNewFrameCount(aNewFrameCount)
-  {
-    MOZ_ASSERT(aImage);
-  }
-
-  NS_IMETHOD Run()
-  {
-    mImage->OnAddedFrame(mNewFrameCount);
-    return NS_OK;
-  }
-
-private:
-  RefPtr<RasterImage> mImage;
-  uint32_t mNewFrameCount;
-};
-
-void
-RasterImage::OnAddedFrame(uint32_t aNewFrameCount)
-{
-  if (!NS_IsMainThread()) {
-    nsCOMPtr<nsIRunnable> runnable =
-      new OnAddedFrameRunnable(this, aNewFrameCount);
-    NS_DispatchToMainThread(runnable);
-    return;
-  }
-
-  MOZ_ASSERT(aNewFrameCount <= mFrameCount + 1, "Skipped a frame?");
-
-  if (mError) {
-    return;  // We're in an error state, possibly due to OOM. Bail.
-  }
-
-  if (aNewFrameCount > mFrameCount) {
-    mFrameCount = aNewFrameCount;
-
-    if (aNewFrameCount == 2) {
-      MOZ_ASSERT(mAnimationState, "Should already have animation state");
-
-      // We may be able to start animating.
-      if (mPendingAnimation && ShouldAnimate()) {
-        StartAnimation();
-      }
-    }
-  }
-}
-
 bool
 RasterImage::SetMetadata(const ImageMetadata& aMetadata,
                          bool aFromMetadataDecode)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mError) {
     return true;
@@ -910,17 +858,17 @@ 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 = !mAnimationState || GetNumFrames() < 2;
+  mPendingAnimation = !mAnimationState || mAnimationState->KnownFrameCount() < 1;
   if (mPendingAnimation) {
     return NS_OK;
   }
 
   // Don't bother to animate if we're displaying the first frame forever.
   if (GetCurrentFrameIndex() == 0 &&
       mAnimationState->FirstFrameTimeout() == FrameTimeout::Forever()) {
     mAnimationFinished = true;
@@ -1649,17 +1597,19 @@ RasterImage::HandleErrorWorker::Run()
   mImage->DoError();
 
   return NS_OK;
 }
 
 bool
 RasterImage::ShouldAnimate()
 {
-  return ImageResource::ShouldAnimate() && GetNumFrames() >= 2 &&
+  return ImageResource::ShouldAnimate() &&
+         mAnimationState &&
+         mAnimationState->KnownFrameCount() >= 1 &&
          !mAnimationFinished;
 }
 
 #ifdef DEBUG
 NS_IMETHODIMP
 RasterImage::GetFramesNotified(uint32_t* aFramesNotified)
 {
   NS_ENSURE_ARG_POINTER(aFramesNotified);
@@ -1668,31 +1618,44 @@ RasterImage::GetFramesNotified(uint32_t*
 
   return NS_OK;
 }
 #endif
 
 void
 RasterImage::NotifyProgress(Progress aProgress,
                             const IntRect& aInvalidRect /* = IntRect() */,
+                            const Maybe<uint32_t>& aFrameCount /* = Nothing() */,
                             SurfaceFlags aSurfaceFlags
                               /* = DefaultSurfaceFlags() */)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // Ensure that we stay alive long enough to finish notifying.
   RefPtr<RasterImage> image = this;
 
   const bool wasDefaultFlags = aSurfaceFlags == DefaultSurfaceFlags();
 
   if (!aInvalidRect.IsEmpty() && wasDefaultFlags) {
     // Update our image container since we're invalidating.
     UpdateImageContainer();
   }
 
+  // We may have decoded new animation frames; update our animation state.
+  MOZ_ASSERT_IF(aFrameCount && *aFrameCount > 1, mAnimationState);
+  if (mAnimationState && aFrameCount) {
+    mAnimationState->UpdateKnownFrameCount(*aFrameCount);
+  }
+
+  // If we should start animating right now, do so.
+  if (mAnimationState && aFrameCount == Some(1u) &&
+      mPendingAnimation && ShouldAnimate()) {
+    StartAnimation();
+  }
+
   // Tell the observers what happened.
   image->mProgressTracker->SyncNotifyProgress(aProgress, aInvalidRect);
 }
 
 void
 RasterImage::FinalizeDecoder(Decoder* aDecoder)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -1722,26 +1685,30 @@ 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 (mAnimationState) {
-      mAnimationState->SetDoneDecoding(true);
-    }
   }
 
   // Send out any final notifications.
   NotifyProgress(aDecoder->TakeProgress(),
                  aDecoder->TakeInvalidRect(),
+                 aDecoder->TakeCompleteFrameCount(),
                  aDecoder->GetSurfaceFlags());
 
+  if (mHasBeenDecoded && mAnimationState) {
+    // We're done decoding and our AnimationState has been notified about all
+    // our frames, so let it know not to expect anymore.
+    mAnimationState->SetDoneDecoding(true);
+  }
+
   if (!wasMetadata && aDecoder->ChunkCount()) {
     Telemetry::Accumulate(Telemetry::IMAGE_DECODE_CHUNKS,
                           aDecoder->ChunkCount());
   }
 
   if (done) {
     // Do some telemetry if this isn't a metadata decode.
     if (!wasMetadata) {
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -157,47 +157,47 @@ public:
 #endif
 
   virtual nsresult StartAnimation() override;
   virtual nsresult StopAnimation() override;
 
   // Methods inherited from Image
   virtual void OnSurfaceDiscarded() override;
 
-  /* The total number of frames in this image. */
-  uint32_t GetNumFrames() const { return mFrameCount; }
-
   virtual size_t SizeOfSourceWithComputedFallback(MallocSizeOf aMallocSizeOf)
     const override;
   virtual void CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
                                      MallocSizeOf aMallocSizeOf) const override;
 
   /* Triggers discarding. */
   void Discard();
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Decoder callbacks.
   //////////////////////////////////////////////////////////////////////////////
 
-  void OnAddedFrame(uint32_t aNewFrameCount);
-
   /**
    * Sends the provided progress notifications to ProgressTracker.
    *
    * Main-thread only.
    *
    * @param aProgress    The progress notifications to send.
    * @param aInvalidRect An invalidation rect to send.
+   * @param aFrameCount  If Some(), an updated count of the number of frames of
+   *                     animation the decoder has finished decoding so far. This
+   *                     is a lower bound for the total number of animation
+   *                     frames this image has.
    * @param aFlags       The surface flags used by the decoder that generated
    *                     these notifications, or DefaultSurfaceFlags() if the
    *                     notifications don't come from a decoder.
    */
   void NotifyProgress(Progress aProgress,
-                      const nsIntRect& aInvalidRect = nsIntRect(),
+                      const gfx::IntRect& aInvalidRect = nsIntRect(),
+                      const Maybe<uint32_t>& aFrameCount = Nothing(),
                       SurfaceFlags aSurfaceFlags = DefaultSurfaceFlags());
 
   /**
    * Records telemetry and does final teardown of the provided decoder.
    *
    * Main-thread only.
    */
   void FinalizeDecoder(Decoder* aDecoder);
@@ -379,19 +379,16 @@ private: // data
 
 #ifdef DEBUG
   uint32_t                       mFramesNotified;
 #endif
 
   // The source data for this image.
   NotNull<RefPtr<SourceBuffer>>  mSourceBuffer;
 
-  // The number of frames this image has.
-  uint32_t                   mFrameCount;
-
   // Boolean flags (clustered together to conserve space):
   bool                       mHasSize:1;       // Has SetSize() been called?
   bool                       mTransient:1;     // Is the image short-lived?
   bool                       mSyncLoad:1;      // Are we loading synchronously?
   bool                       mDiscardable:1;   // Is container discardable?
   bool                       mHasSourceData:1; // Do we have source data?
   bool                       mHasBeenDecoded:1; // Decoded at least once?