# HG changeset patch # User Andrew Osmond # Date 1519842892 18000 # Node ID 209a5b6b89b9f3f97c383203417809a862d156cd # Parent 30d9ace26e6630b0888eeb23a3b5c153dd14a9f4 Bug 523950 - Part 7. Add AnimatedFrameBuffer to manage storage and decoding of frames in an animation. r=tnikkel diff --git a/image/AnimationFrameBuffer.cpp b/image/AnimationFrameBuffer.cpp new file mode 100644 --- /dev/null +++ b/image/AnimationFrameBuffer.cpp @@ -0,0 +1,292 @@ +/* -*- 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 "AnimationFrameBuffer.h" +#include "mozilla/Move.h" // for Move + +namespace mozilla { +namespace image { + +AnimationFrameBuffer::AnimationFrameBuffer() + : mThreshold(0) + , mBatch(0) + , mPending(0) + , mAdvance(0) + , mInsertIndex(0) + , mGetIndex(0) + , mSizeKnown(false) +{ } + +void +AnimationFrameBuffer::Initialize(size_t aThreshold, + size_t aBatch, + size_t aStartFrame) +{ + MOZ_ASSERT(mThreshold == 0); + MOZ_ASSERT(mBatch == 0); + MOZ_ASSERT(mPending == 0); + MOZ_ASSERT(mAdvance == 0); + MOZ_ASSERT(mFrames.IsEmpty()); + + mThreshold = aThreshold; + mBatch = aBatch; + mAdvance = aStartFrame; + + if (mBatch > SIZE_MAX/4) { + // Batch size is so big, we will just end up decoding the whole animation. + mBatch = SIZE_MAX/4; + } else if (mBatch < 1) { + // Never permit a batch size smaller than 1. We always want to be asking for + // at least one frame to start. + mBatch = 1; + } + + // To simplify the code, we have the assumption that the threshold for + // entering discard-after-display mode is at least twice the batch size (since + // that is the most frames-pending-decode we will request) + 1 for the current + // frame. That way the redecoded frames being inserted will never risk + // overlapping the frames we will discard due to the animation progressing. + // That may cause us to use a little more memory than we want but that is an + // acceptable tradeoff for simplicity. + size_t minThreshold = 2 * mBatch + 1; + if (mThreshold < minThreshold) { + mThreshold = minThreshold; + } + + // The maximum number of frames we should ever have decoded at one time is + // twice the batch. That is a good as number as any to start our decoding at. + mPending = mBatch * 2; +} + +bool +AnimationFrameBuffer::Insert(RawAccessFrameRef&& aFrame) +{ + // We should only insert new frames if we actually asked for them. + MOZ_ASSERT(mPending > 0); + + if (mSizeKnown) { + // We only insert after the size is known if we are repeating the animation + // and we did not keep all of the frames. Replace whatever is there + // (probably an empty frame) with the new frame. + MOZ_ASSERT(MayDiscard()); + MOZ_ASSERT(mInsertIndex < mFrames.Length()); + + if (mInsertIndex > 0) { + MOZ_ASSERT(!mFrames[mInsertIndex]); + mFrames[mInsertIndex] = Move(aFrame); + } + } else { + if (mInsertIndex == mFrames.Length()) { + // We are still on the first pass of the animation decoding, so this is + // the first time we have seen this frame. + mFrames.AppendElement(Move(aFrame)); + } else if (mInsertIndex > 0) { + // We were forced to restart an animation before we decoded the last + // frame. Thus we might need to insert, even on a "first pass." + MOZ_ASSERT(mInsertIndex < mFrames.Length()); + MOZ_ASSERT(!mFrames[mInsertIndex]); + mFrames[mInsertIndex] = Move(aFrame); + } + + if (mInsertIndex == mThreshold) { + // We just tripped over the threshold, and on the first pass of the + // decoding; this is our chance to do any clearing of already displayed + // frames. After this, we only need to release as we advance. + MOZ_ASSERT(MayDiscard()); + MOZ_ASSERT(mGetIndex < mInsertIndex); + for (size_t i = 1; i < mGetIndex; ++i) { + RawAccessFrameRef discard = Move(mFrames[i]); + } + } + } + + MOZ_ASSERT(mFrames[mInsertIndex]); + ++mInsertIndex; + + // Ensure we only request more decoded frames if we actually need them. If we + // need to advance to a certain point in the animation on behalf of the owner, + // then do so. This ensures we keep decoding. If the batch size is really + // small (i.e. 1), it is possible advancing will request the decoder to + // "restart", but we haven't told it to stop yet. Note that we skip the first + // insert because we actually start "advanced" to the first frame anyways. + bool continueDecoding = --mPending > 0; + if (mAdvance > 0 && mInsertIndex > 1) { + continueDecoding |= AdvanceInternal(); + --mAdvance; + } + return continueDecoding; +} + +bool +AnimationFrameBuffer::MarkComplete() +{ + // We reached the end of the animation, the next frame we get, if we get + // another, will be the first frame again. + MOZ_ASSERT(mInsertIndex == mFrames.Length()); + mInsertIndex = 0; + + // Since we only request advancing when we want to resume at a certain point + // in the animation, we should never exceed the number of frames. + MOZ_ASSERT(mAdvance == 0); + + if (!mSizeKnown) { + // We just received the last frame in the animation. Compact the frame array + // because we know we won't need to grow beyond here. + mSizeKnown = true; + mFrames.Compact(); + + if (!MayDiscard()) { + // If we did not meet the threshold, then we know we want to keep all of the + // frames. If we also hit the last frame, we don't want to ask for more. + mPending = 0; + } + } + + return mPending > 0; +} + +DrawableFrameRef +AnimationFrameBuffer::Get(size_t aFrame) +{ + // We should not have asked for a frame if we never inserted. + if (mFrames.IsEmpty()) { + MOZ_ASSERT_UNREACHABLE("Calling Get() when we have no frames"); + return DrawableFrameRef(); + } + + // If we don't have that frame, return an empty frame ref. + if (aFrame >= mFrames.Length()) { + return DrawableFrameRef(); + } + + // We've got the requested frame because we are not discarding frames. While + // we typically should have not run out of frames since we ask for more before + // we want them, it is possible the decoder is behind. + if (!mFrames[aFrame]) { + MOZ_ASSERT(MayDiscard()); + return DrawableFrameRef(); + } + + // If we are advancing on behalf of the animation, we don't expect it to be + // getting any frames (besides the first) until we get the desired frame. + MOZ_ASSERT(aFrame == 0 || mAdvance == 0); + return mFrames[aFrame]->DrawableRef(); +} + +bool +AnimationFrameBuffer::AdvanceTo(size_t aExpectedFrame) +{ + // The owner should only be advancing once it has reached the requested frame + // in the animation. + MOZ_ASSERT(mAdvance == 0); + bool restartDecoder = AdvanceInternal(); + // Advancing should always be successful, as it should only happen after the + // owner has accessed the next (now current) frame. + MOZ_ASSERT(mGetIndex == aExpectedFrame); + return restartDecoder; +} + +bool +AnimationFrameBuffer::AdvanceInternal() +{ + // We should not have advanced if we never inserted. + if (mFrames.IsEmpty()) { + MOZ_ASSERT_UNREACHABLE("Calling Advance() when we have no frames"); + return false; + } + + // We only want to change the current frame index if we have advanced. This + // means either a higher frame index, or going back to the beginning. + size_t framesLength = mFrames.Length(); + // We should never have advanced beyond the frame buffer. + MOZ_ASSERT(mGetIndex < framesLength); + // We should never advance if the current frame is null -- it needs to know + // the timeout from it at least to know when to advance. + MOZ_ASSERT(mFrames[mGetIndex]); + if (++mGetIndex == framesLength) { + MOZ_ASSERT(mSizeKnown); + mGetIndex = 0; + } + // The owner should have already accessed the next frame, so it should also + // be available. + MOZ_ASSERT(mFrames[mGetIndex]); + + // If we moved forward, that means we can remove the previous frame, assuming + // that frame is not the first frame. If we looped and are back at the first + // frame, we can remove the last frame. + if (MayDiscard()) { + RawAccessFrameRef discard; + if (mGetIndex > 1) { + discard = Move(mFrames[mGetIndex - 1]); + } else if (mGetIndex == 0) { + MOZ_ASSERT(mSizeKnown && framesLength > 1); + discard = Move(mFrames[framesLength - 1]); + } + } + + if (!mSizeKnown || MayDiscard()) { + // Calculate how many frames we have requested ahead of the current frame. + size_t buffered = mPending; + if (mGetIndex > mInsertIndex) { + // It wrapped around and we are decoding the beginning again before the + // the display has finished the loop. + MOZ_ASSERT(mSizeKnown); + buffered += mInsertIndex + framesLength - mGetIndex - 1; + } else { + buffered += mInsertIndex - mGetIndex - 1; + } + + if (buffered < mBatch) { + // If we have fewer frames than the batch size, then ask for more. If we + // do not have any pending, then we know that there is no active decoding. + mPending += mBatch; + return mPending == mBatch; + } + } + + return false; +} + +bool +AnimationFrameBuffer::Reset() +{ + // The animation needs to start back at the beginning. + mGetIndex = 0; + mAdvance = 0; + + if (!MayDiscard()) { + // If we haven't crossed the threshold, then we know by definition we have + // not discarded any frames. If we previously requested more frames, but + // it would have been more than we would have buffered otherwise, we can + // stop the decoding after one more frame. + if (mPending > 1 && mInsertIndex - 1 >= mBatch * 2) { + MOZ_ASSERT(!mSizeKnown); + mPending = 1; + } + + // Either the decoder is still running, or we have enough frames already. + // No need for us to restart it. + return false; + } + + // If we are over the threshold, then we know we will have missing frames in + // our buffer. The easiest thing to do is to drop everything but the first + // frame and go back to the initial state. + bool restartDecoder = mPending == 0; + mInsertIndex = 0; + mPending = 2 * mBatch; + + // Discard all frames besides the first, because the decoder always expects + // that when it re-inserts a frame, it is not present. (It doesn't re-insert + // the first frame.) + for (size_t i = 1; i < mFrames.Length(); ++i) { + RawAccessFrameRef discard = Move(mFrames[i]); + } + + return restartDecoder; +} + +} // namespace image +} // namespace mozilla diff --git a/image/AnimationFrameBuffer.h b/image/AnimationFrameBuffer.h new file mode 100644 --- /dev/null +++ b/image/AnimationFrameBuffer.h @@ -0,0 +1,195 @@ +/* -*- 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_image_AnimationFrameBuffer_h +#define mozilla_image_AnimationFrameBuffer_h + +#include "ISurfaceProvider.h" + +namespace mozilla { +namespace image { + +/** + * An AnimationFrameBuffer owns the frames outputted by an animated image + * decoder as well as directing its owner on how to drive the decoder, + * whether to produce more or to stop. + * + * Based upon its given configuration parameters, it will retain up to a + * certain number of frames in the buffer before deciding to discard previous + * frames, and relying upon the decoder to recreate older frames when the + * animation loops. It will also request that the decoder stop producing more + * frames when the display of the frames are far behind -- this allows other + * tasks and images which require decoding to take execution priority. + * + * The desire is that smaller animated images should be kept completely in + * memory while larger animated images should only keep a certain number of + * frames to minimize our memory footprint at the cost of CPU. + */ +class AnimationFrameBuffer final +{ +public: + AnimationFrameBuffer(); + + /** + * Configure the frame buffer with a particular threshold and batch size. Note + * that the frame buffer may adjust the given values. + * + * @param aThreshold Maximum number of frames that may be stored in the frame + * buffer before it may discard already displayed frames. + * Once exceeded, it will discard the previous frame to the + * current frame whenever Advance is called. It always + * retains the first frame. + * + * @param aBatch Number of frames we request to be decoded each time it + * decides we need more. + * + * @param aStartFrame The starting frame for the animation. The frame buffer + * will auto-advance (and thus keep the decoding pipeline + * going) until it has reached this frame. Useful when the + * animation was progressing, but the surface was + * discarded, and we had to redecode. + */ + void Initialize(size_t aThreshold, size_t aBatch, size_t aStartFrame); + + /** + * Access a specific frame from the frame buffer. It should generally access + * frames in sequential order, increasing in tandem with AdvanceTo calls. The + * first frame may be accessed at any time. The access order should start with + * the same value as that given in Initialize (aStartFrame). + * + * @param aFrame The frame index to access. + * + * @returns The frame, if available. + */ + DrawableFrameRef Get(size_t aFrame); + + /** + * Inserts a frame into the frame buffer. If it has yet to fully decode the + * animated image yet, then it will append the frame to its internal buffer. + * If it has been fully decoded, it will replace the next frame in its buffer + * with the given frame. + * + * Once we have a sufficient number of frames buffered relative to the + * currently displayed frame, it will return false to indicate the caller + * should stop decoding. + * + * @param aFrame The frame to insert into the buffer. + * + * @returns True if the decoder should decode another frame. + */ + bool Insert(RawAccessFrameRef&& aFrame); + + /** + * This should be called after the last frame has been inserted. If the buffer + * is discarding old frames, it may request more frames to be decoded. In this + * case that means the decoder should start again from the beginning. This + * return value should be used in preference to that of the Insert call. + * + * @returns True if the decoder should decode another frame. + */ + bool MarkComplete(); + + /** + * Advance the currently displayed frame of the frame buffer. If it reaches + * the end, it will loop back to the beginning. It should not be called unless + * a call to Get has returned a valid frame for the next frame index. + * + * As we advance, the number of frames we have buffered ahead of the current + * will shrink. Once that becomes too few, we will request a batch-sized set + * of frames to be decoded from the decoder. + * + * @param aExpectedFrame The frame we expect to have advanced to. This is + * used for confirmation purposes (e.g. asserts). + * + * @returns True if the caller should restart the decoder. + */ + bool AdvanceTo(size_t aExpectedFrame); + + /** + * Resets the currently displayed frame of the frame buffer to the beginning. + * If the buffer is discarding old frames, it will actually discard all frames + * besides the first. + * + * @returns True if the caller should restart the decoder. + */ + bool Reset(); + + /** + * @returns True if frames post-advance may be discarded and redecoded on + * demand, else false. + */ + bool MayDiscard() const { return mFrames.Length() > mThreshold; } + + /** + * @returns True if the frame buffer was ever marked as complete. This implies + * that the total number of frames is known and may be gotten from + * Frames().Length(). + */ + bool SizeKnown() const { return mSizeKnown; } + + /** + * @returns The current frame index we have advanced to. + */ + size_t Displayed() const { return mGetIndex; } + + /** + * @returns Outstanding frames desired from the decoder. + */ + size_t PendingDecode() const { return mPending; } + + /** + * @returns Outstanding frames to advance internally. + */ + size_t PendingAdvance() const { return mAdvance; } + + /** + * @returns Number of frames we request to be decoded each time it decides we + * need more. + */ + size_t Batch() const { return mBatch; } + + /** + * @returns Maximum number of frames before we start discarding previous + * frames post-advance. + */ + size_t Threshold() const { return mThreshold; } + + /** + * @returns The frames of this animation, in order. May contain empty indices. + */ + const nsTArray& Frames() const { return mFrames; } + +private: + bool AdvanceInternal(); + + /// The frames of this animation, in order, but may have holes if discarding. + nsTArray mFrames; + + // The maximum number of frames we can have before discarding. + size_t mThreshold; + + // The minimum number of frames that we want buffered ahead of the display. + size_t mBatch; + + // The number of frames to decode before we stop. + size_t mPending; + + // The number of frames we need to auto-advance to synchronize with the caller. + size_t mAdvance; + + // The mFrames index in which to insert the next decoded frame. + size_t mInsertIndex; + + // The mFrames index that we have advanced to. + size_t mGetIndex; + + // True if the total number of frames is known. + bool mSizeKnown; +}; + +} // namespace image +} // namespace mozilla + +#endif // mozilla_image_AnimationFrameBuffer_h diff --git a/image/moz.build b/image/moz.build --- a/image/moz.build +++ b/image/moz.build @@ -49,16 +49,17 @@ EXPORTS += [ 'imgRequest.h', 'imgRequestProxy.h', 'IProgressObserver.h', 'Orientation.h', 'SurfaceCacheUtils.h', ] UNIFIED_SOURCES += [ + 'AnimationFrameBuffer.cpp', 'AnimationSurfaceProvider.cpp', 'ClippedImage.cpp', 'DecodedSurfaceProvider.cpp', 'DecodePool.cpp', 'Decoder.cpp', 'DecoderFactory.cpp', 'DynamicImage.cpp', 'FrameAnimator.cpp',