summaryrefslogtreecommitdiff
path: root/image/AnimationFrameBuffer.cpp
diff options
context:
space:
mode:
authorMartok <martok@martoks-place.de>2022-12-31 22:55:46 +0100
committerMartok <martok@martoks-place.de>2023-01-04 00:49:04 +0100
commitb618b3fb767e61f73732a0385f733357571314b0 (patch)
treec7e53031c59f85d10ee167dc999696edc46eaa79 /image/AnimationFrameBuffer.cpp
parente7fd4ba61bec3736df2f50eb0e7b215a805dde06 (diff)
downloaduxp-b618b3fb767e61f73732a0385f733357571314b0.tar.gz
Issue #2073 - m-c 523950: Discard decoded frames of very large GIF animations (squashed)
Controlled by image.animated.decode-on-demand.threshold-kb, default 256MB Includes squashed bugfixes/regressions: - m-c 1444537: Shutting down the decode pool should make animated decoders bail early - m-c 1628606: Make sure to mark the surface cache entry available before sending the frame complete notification - m-c 1502275: Skip recreating the decoder after redecode errors if an animated image is reset - m-c 1443232: Don't insert frames into our AnimationFrameBuffer that we consider in error and unusable
Diffstat (limited to 'image/AnimationFrameBuffer.cpp')
-rw-r--r--image/AnimationFrameBuffer.cpp324
1 files changed, 324 insertions, 0 deletions
diff --git a/image/AnimationFrameBuffer.cpp b/image/AnimationFrameBuffer.cpp
new file mode 100644
index 0000000000..5d8b672f66
--- /dev/null
+++ b/image/AnimationFrameBuffer.cpp
@@ -0,0 +1,324 @@
+/* -*- 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)
+ , mRedecodeError(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());
+
+ // The first decode produced fewer frames than the redecodes, presumably
+ // because it hit an out-of-memory error which later attempts avoided. Just
+ // stop the animation because we can't tell the image that we have more
+ // frames now.
+ if (mInsertIndex >= mFrames.Length()) {
+ mRedecodeError = true;
+ mPending = 0;
+ return false;
+ }
+
+ 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));
+
+ if (mInsertIndex == mThreshold) {
+ // We just tripped over the threshold for the first time. This is our
+ // chance to do any clearing of already displayed frames. After this,
+ // we only need to release as we advance or force a restart.
+ MOZ_ASSERT(MayDiscard());
+ MOZ_ASSERT(mGetIndex < mInsertIndex);
+ for (size_t i = 1; i < mGetIndex; ++i) {
+ RawAccessFrameRef discard = Move(mFrames[i]);
+ }
+ }
+ } else if (mInsertIndex > 0) {
+ // We were forced to restart an animation before we decoded the last
+ // frame. If we were discarding frames, then we tossed what we had
+ // except for the first frame.
+ MOZ_ASSERT(mInsertIndex < mFrames.Length());
+ MOZ_ASSERT(!mFrames[mInsertIndex]);
+ MOZ_ASSERT(MayDiscard());
+ mFrames[mInsertIndex] = Move(aFrame);
+ } else { // mInsertIndex == 0
+ // We were forced to restart an animation before we decoded the last
+ // frame. We don't need the redecoded first frame because we always keep
+ // the original.
+ MOZ_ASSERT(MayDiscard());
+ }
+
+ 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 may have stopped decoding at a different point in the animation than we
+ // did previously. That means the decoder likely hit a new error, e.g. OOM.
+ // This will prevent us from advancing as well, because we are missing the
+ // required frames to blend.
+ //
+ // XXX(aosmond): In an ideal world, we would be generating full frames, and
+ // the consumer of our data doesn't care about our internal state. It simply
+ // knows about the first frame, the current frame, and how long to display the
+ // current frame.
+ if (NS_WARN_IF(mInsertIndex != mFrames.Length())) {
+ MOZ_ASSERT(mSizeKnown);
+ mRedecodeError = true;
+ mPending = 0;
+ }
+
+ // We reached the end of the animation, the next frame we get, if we get
+ // another, will be the first frame again.
+ 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 (!mRedecodeError && (!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;
+ }
+
+ // 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]);
+ }
+
+ mInsertIndex = 0;
+
+ // If we hit an error after redecoding, we never want to restart decoding.
+ if (mRedecodeError) {
+ MOZ_ASSERT(mPending == 0);
+ return false;
+ }
+
+ bool restartDecoder = mPending == 0;
+ mPending = 2 * mBatch;
+ return restartDecoder;
+}
+
+} // namespace image
+} // namespace mozilla