summaryrefslogtreecommitdiffstats
path: root/image/test/gtest/TestAnimationFrameBuffer.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--image/test/gtest/TestAnimationFrameBuffer.cpp895
1 files changed, 895 insertions, 0 deletions
diff --git a/image/test/gtest/TestAnimationFrameBuffer.cpp b/image/test/gtest/TestAnimationFrameBuffer.cpp
new file mode 100644
index 0000000000..78186e5066
--- /dev/null
+++ b/image/test/gtest/TestAnimationFrameBuffer.cpp
@@ -0,0 +1,895 @@
+/* -*- 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 <utility>
+
+#include "AnimationFrameBuffer.h"
+#include "Common.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+static already_AddRefed<imgFrame> CreateEmptyFrame(
+ const gfx::IntSize& aSize = gfx::IntSize(1, 1),
+ const gfx::IntRect& aFrameRect = gfx::IntRect(0, 0, 1, 1),
+ bool aCanRecycle = true) {
+ RefPtr<imgFrame> frame = new imgFrame();
+ AnimationParams animParams{aFrameRect, FrameTimeout::Forever(),
+ /* aFrameNum */ 1, BlendMethod::OVER,
+ DisposalMethod::NOT_SPECIFIED};
+ nsresult rv =
+ frame->InitForDecoder(aSize, mozilla::gfx::SurfaceFormat::OS_RGBA, false,
+ Some(animParams), aCanRecycle);
+ EXPECT_NS_SUCCEEDED(rv);
+ RawAccessFrameRef frameRef = frame->RawAccessRef();
+ // Normally the blend animation filter would set the dirty rect, but since
+ // we aren't producing an actual animation here, we need to fake it.
+ frame->SetDirtyRect(aFrameRect);
+ frame->Finish();
+ return frame.forget();
+}
+
+static bool ReinitForRecycle(RawAccessFrameRef& aFrame) {
+ if (!aFrame) {
+ return false;
+ }
+
+ AnimationParams animParams{aFrame->GetRect(), FrameTimeout::Forever(),
+ /* aFrameNum */ 1, BlendMethod::OVER,
+ DisposalMethod::NOT_SPECIFIED};
+ return NS_SUCCEEDED(aFrame->InitForDecoderRecycle(animParams));
+}
+
+static void PrepareForDiscardingQueue(AnimationFrameRetainedBuffer& aQueue) {
+ ASSERT_EQ(size_t(0), aQueue.Size());
+ ASSERT_LT(size_t(1), aQueue.Batch());
+
+ AnimationFrameBuffer::InsertStatus status = aQueue.Insert(CreateEmptyFrame());
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ while (true) {
+ status = aQueue.Insert(CreateEmptyFrame());
+ bool restartDecoder = aQueue.AdvanceTo(aQueue.Size() - 1);
+ EXPECT_FALSE(restartDecoder);
+
+ if (status == AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE) {
+ break;
+ }
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+ }
+
+ EXPECT_EQ(aQueue.Threshold(), aQueue.Size());
+}
+
+static void VerifyDiscardingQueueContents(
+ AnimationFrameDiscardingQueue& aQueue) {
+ auto frames = aQueue.Display();
+ for (auto i : frames) {
+ EXPECT_TRUE(i != nullptr);
+ }
+}
+
+static void VerifyInsertInternal(AnimationFrameBuffer& aQueue,
+ imgFrame* aFrame) {
+ // Determine the frame index where we just inserted the frame.
+ size_t frameIndex;
+ if (aQueue.MayDiscard()) {
+ const AnimationFrameDiscardingQueue& queue =
+ *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
+ frameIndex = queue.PendingInsert() == 0 ? queue.Size() - 1
+ : queue.PendingInsert() - 1;
+ } else {
+ ASSERT_FALSE(aQueue.SizeKnown());
+ frameIndex = aQueue.Size() - 1;
+ }
+
+ // Make sure we can get the frame from that index.
+ RefPtr<imgFrame> frame = aQueue.Get(frameIndex, false);
+ EXPECT_EQ(aFrame, frame.get());
+}
+
+static void VerifyAdvance(AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
+ bool aExpectedRestartDecoder) {
+ RefPtr<imgFrame> oldFrame;
+ size_t totalRecycled;
+ if (aQueue.IsRecycling()) {
+ AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ oldFrame = queue.Get(queue.Displayed(), false);
+ totalRecycled = queue.Recycle().size();
+ }
+
+ bool restartDecoder = aQueue.AdvanceTo(aExpectedFrame);
+ EXPECT_EQ(aExpectedRestartDecoder, restartDecoder);
+
+ if (aQueue.IsRecycling()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_FALSE(queue.Recycle().back().mDirtyRect.IsEmpty());
+ EXPECT_TRUE(
+ queue.Recycle().back().mDirtyRect.Contains(oldFrame->GetDirtyRect()));
+ EXPECT_EQ(totalRecycled + 1, queue.Recycle().size());
+ EXPECT_EQ(oldFrame.get(), queue.Recycle().back().mFrame.get());
+ }
+}
+
+static void VerifyInsertAndAdvance(
+ AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
+ AnimationFrameBuffer::InsertStatus aExpectedStatus) {
+ // Insert the decoded frame.
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status =
+ aQueue.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(aExpectedStatus, status);
+ EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
+ VerifyInsertInternal(aQueue, frame);
+
+ // Advance the display frame.
+ bool expectedRestartDecoder =
+ aExpectedStatus == AnimationFrameBuffer::InsertStatus::YIELD;
+ VerifyAdvance(aQueue, aExpectedFrame, expectedRestartDecoder);
+}
+
+static void VerifyMarkComplete(
+ AnimationFrameBuffer& aQueue, bool aExpectedContinue,
+ const gfx::IntRect& aRefreshArea = gfx::IntRect(0, 0, 1, 1)) {
+ if (aQueue.IsRecycling() && !aQueue.SizeKnown()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_EQ(queue.FirstFrame()->GetRect(), queue.FirstFrameRefreshArea());
+ }
+
+ bool keepDecoding = aQueue.MarkComplete(aRefreshArea);
+ EXPECT_EQ(aExpectedContinue, keepDecoding);
+
+ if (aQueue.IsRecycling()) {
+ const AnimationFrameRecyclingQueue& queue =
+ *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
+ EXPECT_EQ(aRefreshArea, queue.FirstFrameRefreshArea());
+ }
+}
+
+static void VerifyInsert(AnimationFrameBuffer& aQueue,
+ AnimationFrameBuffer::InsertStatus aExpectedStatus) {
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status =
+ aQueue.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(aExpectedStatus, status);
+ EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
+ VerifyInsertInternal(aQueue, frame);
+}
+
+static void VerifyReset(AnimationFrameBuffer& aQueue, bool aExpectedContinue,
+ const imgFrame* aFirstFrame) {
+ bool keepDecoding = aQueue.Reset();
+ EXPECT_EQ(aExpectedContinue, keepDecoding);
+ EXPECT_EQ(aQueue.Batch() * 2, aQueue.PendingDecode());
+ EXPECT_EQ(aFirstFrame, aQueue.Get(0, true));
+
+ if (!aQueue.MayDiscard()) {
+ const AnimationFrameRetainedBuffer& queue =
+ *static_cast<AnimationFrameRetainedBuffer*>(&aQueue);
+ EXPECT_EQ(aFirstFrame, queue.Frames()[0].get());
+ EXPECT_EQ(aFirstFrame, aQueue.Get(0, false));
+ } else {
+ const AnimationFrameDiscardingQueue& queue =
+ *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
+ EXPECT_EQ(size_t(0), queue.PendingInsert());
+ EXPECT_EQ(size_t(0), queue.Display().size());
+ EXPECT_EQ(aFirstFrame, queue.FirstFrame());
+ EXPECT_EQ(nullptr, aQueue.Get(0, false));
+ }
+}
+
+class ImageAnimationFrameBuffer : public ::testing::Test {
+ public:
+ ImageAnimationFrameBuffer() {}
+
+ private:
+ AutoInitializeImageLib mInit;
+};
+
+TEST_F(ImageAnimationFrameBuffer, RetainedInitialState) {
+ const size_t kThreshold = 800;
+ const size_t kBatch = 100;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kThreshold, buffer.Threshold());
+ EXPECT_EQ(kBatch, buffer.Batch());
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_FALSE(buffer.MayDiscard());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, ThresholdTooSmall) {
+ const size_t kThreshold = 0;
+ const size_t kBatch = 10;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kBatch * 2 + 1, buffer.Threshold());
+ EXPECT_EQ(kBatch, buffer.Batch());
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, BatchTooSmall) {
+ const size_t kThreshold = 10;
+ const size_t kBatch = 0;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ EXPECT_EQ(kThreshold, buffer.Threshold());
+ EXPECT_EQ(size_t(1), buffer.Batch());
+ EXPECT_EQ(size_t(2), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, BatchTooBig) {
+ const size_t kThreshold = 50;
+ const size_t kBatch = SIZE_MAX;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+
+ // The rounding is important here (e.g. SIZE_MAX/4 * 2 != SIZE_MAX/2).
+ EXPECT_EQ(SIZE_MAX / 4, buffer.Batch());
+ EXPECT_EQ(buffer.Batch() * 2 + 1, buffer.Threshold());
+ EXPECT_EQ(buffer.Batch() * 2, buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+}
+
+TEST_F(ImageAnimationFrameBuffer, FinishUnderBatchAndThreshold) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 10;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+ const auto& frames = buffer.Frames();
+
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+
+ RefPtr<imgFrame> firstFrame;
+ for (size_t i = 0; i < 5; ++i) {
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ auto status = buffer.Insert(RefPtr<imgFrame>(frame));
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_EQ(buffer.Size(), i + 1);
+
+ if (i == 4) {
+ EXPECT_EQ(size_t(15), buffer.PendingDecode());
+ bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(keepDecoding);
+ EXPECT_TRUE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_FALSE(buffer.HasRedecodeError());
+ }
+
+ EXPECT_FALSE(buffer.MayDiscard());
+
+ imgFrame* gotFrame = buffer.Get(i, false);
+ EXPECT_EQ(frame.get(), gotFrame);
+ ASSERT_EQ(i + 1, frames.Length());
+ EXPECT_EQ(frame.get(), frames[i].get());
+
+ if (i == 0) {
+ firstFrame = std::move(frame);
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ } else {
+ EXPECT_EQ(i - 1, buffer.Displayed());
+ bool restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_EQ(i, buffer.Displayed());
+ }
+
+ gotFrame = buffer.Get(0, false);
+ EXPECT_EQ(firstFrame.get(), gotFrame);
+ }
+
+ // Loop again over the animation and make sure it is still all there.
+ for (size_t i = 0; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+
+ bool restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+}
+
+TEST_F(ImageAnimationFrameBuffer, FinishMultipleBatchesUnderThreshold) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
+ const auto& frames = buffer.Frames();
+
+ EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
+
+ // Add frames until it tells us to stop.
+ AnimationFrameBuffer::InsertStatus status;
+ do {
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+ } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);
+
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), frames.Length());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Progress through the animation until it lets us decode again.
+ bool restartDecoder = false;
+ size_t i = 0;
+ do {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ if (i > 0) {
+ restartDecoder = buffer.AdvanceTo(i);
+ }
+ ++i;
+ } while (!restartDecoder);
+
+ EXPECT_EQ(size_t(2), buffer.PendingDecode());
+ EXPECT_EQ(size_t(2), buffer.Displayed());
+
+ // Add the last frame.
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(keepDecoding);
+ EXPECT_TRUE(buffer.SizeKnown());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(5), frames.Length());
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Finish progressing through the animation.
+ for (; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Loop again over the animation and make sure it is still all there.
+ for (i = 0; i < frames.Length(); ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Loop to the third frame and then reset the animation.
+ for (i = 0; i < 3; ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ restartDecoder = buffer.AdvanceTo(i);
+ EXPECT_FALSE(restartDecoder);
+ }
+
+ // Since we are below the threshold, we can reset the get index only.
+ // Nothing else should have changed.
+ restartDecoder = buffer.Reset();
+ EXPECT_FALSE(restartDecoder);
+ for (i = 0; i < 5; ++i) {
+ EXPECT_TRUE(buffer.Get(i, false) != nullptr);
+ }
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+}
+
+TEST_F(ImageAnimationFrameBuffer, StartAfterBeginning) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 7;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);
+
+ EXPECT_EQ(kStartFrame, buffer.PendingAdvance());
+
+ // Add frames until it tells us to stop. It should be later than before,
+ // because it auto-advances until its displayed frame is kStartFrame.
+ AnimationFrameBuffer::InsertStatus status;
+ size_t i = 0;
+ do {
+ status = buffer.Insert(CreateEmptyFrame());
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+
+ if (i <= kStartFrame) {
+ EXPECT_EQ(i, buffer.Displayed());
+ EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
+ } else {
+ EXPECT_EQ(kStartFrame, buffer.Displayed());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ }
+
+ i++;
+ } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);
+
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(10), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, StartAfterBeginningAndReset) {
+ const size_t kThreshold = 30;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 7;
+ AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);
+
+ EXPECT_EQ(kStartFrame, buffer.PendingAdvance());
+
+ // Add frames until it tells us to stop. It should be later than before,
+ // because it auto-advances until its displayed frame is kStartFrame.
+ for (size_t i = 0; i < 5; ++i) {
+ AnimationFrameBuffer::InsertStatus status =
+ buffer.Insert(CreateEmptyFrame());
+ EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_FALSE(buffer.SizeKnown());
+ EXPECT_FALSE(buffer.MayDiscard());
+ EXPECT_EQ(i, buffer.Displayed());
+ EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
+ }
+
+ // When we reset the animation, it goes back to the beginning. That means
+ // we can forget about what we were told to advance to at the start. While
+ // we have plenty of frames in our buffer, we still need one more because
+ // in the real scenario, the decoder thread is still running and it is easier
+ // to let it insert its last frame than to coordinate quitting earlier.
+ buffer.Reset();
+ EXPECT_EQ(size_t(0), buffer.Displayed());
+ EXPECT_EQ(size_t(1), buffer.PendingDecode());
+ EXPECT_EQ(size_t(0), buffer.PendingAdvance());
+ EXPECT_EQ(size_t(5), buffer.Size());
+}
+
+static void TestDiscardingQueueLoop(AnimationFrameDiscardingQueue& aQueue,
+ const imgFrame* aFirstFrame,
+ size_t aThreshold, size_t aBatch,
+ size_t aStartFrame) {
+ // We should be advanced right up to the last decoded frame.
+ EXPECT_TRUE(aQueue.MayDiscard());
+ EXPECT_FALSE(aQueue.SizeKnown());
+ EXPECT_EQ(aBatch, aQueue.Batch());
+ EXPECT_EQ(aThreshold, aQueue.PendingInsert());
+ EXPECT_EQ(aThreshold, aQueue.Size());
+ EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ EXPECT_EQ(size_t(3), aQueue.PendingDecode());
+ VerifyDiscardingQueueContents(aQueue);
+
+ // Make sure frames get removed as we advance.
+ VerifyInsertAndAdvance(aQueue, 5,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ VerifyInsertAndAdvance(aQueue, 6,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+
+ // We actually will yield if we are recycling instead of continuing because
+ // the pending calculation is slightly different. We will actually request one
+ // less frame than we have to recycle.
+ if (aQueue.IsRecycling()) {
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ } else {
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ }
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+
+ // We should get throttled if we insert too much.
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ EXPECT_EQ(size_t(2), aQueue.Display().size());
+ EXPECT_EQ(size_t(1), aQueue.PendingDecode());
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
+ EXPECT_EQ(size_t(3), aQueue.Display().size());
+ EXPECT_EQ(size_t(0), aQueue.PendingDecode());
+
+ // We should get restarted if we advance.
+ VerifyAdvance(aQueue, 8, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ VerifyAdvance(aQueue, 9, false);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+
+ // We should continue decoding if we completed, since we are discarding.
+ VerifyMarkComplete(aQueue, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ EXPECT_EQ(size_t(10), aQueue.Size());
+ EXPECT_TRUE(aQueue.SizeKnown());
+ EXPECT_FALSE(aQueue.HasRedecodeError());
+
+ // Insert the first frames of the animation.
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
+ EXPECT_EQ(size_t(0), aQueue.PendingDecode());
+ EXPECT_EQ(size_t(10), aQueue.Size());
+
+ // Advance back at the beginning. The first frame should only match for
+ // display purposes.
+ VerifyAdvance(aQueue, 0, true);
+ EXPECT_EQ(size_t(2), aQueue.PendingDecode());
+ EXPECT_TRUE(aQueue.FirstFrame() != nullptr);
+ EXPECT_TRUE(aQueue.Get(0, false) != nullptr);
+ EXPECT_NE(aQueue.FirstFrame(), aQueue.Get(0, false));
+ EXPECT_EQ(aQueue.FirstFrame(), aQueue.Get(0, true));
+
+ // Reiterate one more time and make it loops back.
+ VerifyInsertAndAdvance(aQueue, 1,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 3,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 4, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 5,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 6, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 7,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 8, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ EXPECT_EQ(size_t(10), aQueue.PendingInsert());
+ VerifyMarkComplete(aQueue, true);
+ EXPECT_EQ(size_t(0), aQueue.PendingInsert());
+
+ VerifyInsertAndAdvance(aQueue, 9,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(aQueue, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(aQueue, 1,
+ AnimationFrameBuffer::InsertStatus::CONTINUE);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingLoop) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingLoop) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+
+ // We should not start with any recycled frames.
+ ASSERT_TRUE(buffer.Recycle().empty());
+
+ TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+
+ // All the frames we inserted should have been recycleable.
+ ASSERT_FALSE(buffer.Recycle().empty());
+ while (!buffer.Recycle().empty()) {
+ gfx::IntRect expectedRect(0, 0, 1, 1);
+ RefPtr<imgFrame> expectedFrame = buffer.Recycle().front().mFrame;
+ EXPECT_FALSE(expectedRect.IsEmpty());
+ EXPECT_TRUE(expectedFrame.get() != nullptr);
+
+ gfx::IntRect gotRect;
+ RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
+ EXPECT_EQ(expectedFrame.get(), gotFrame.get());
+ EXPECT_EQ(expectedRect, gotRect);
+ EXPECT_TRUE(ReinitForRecycle(gotFrame));
+ }
+
+ // Trying to pull a recycled frame when we have nothing should be safe too.
+ gfx::IntRect gotRect;
+ RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
+ EXPECT_TRUE(gotFrame.get() == nullptr);
+ EXPECT_FALSE(ReinitForRecycle(gotFrame));
+}
+
+static void TestDiscardingQueueReset(AnimationFrameDiscardingQueue& aQueue,
+ const imgFrame* aFirstFrame,
+ size_t aThreshold, size_t aBatch,
+ size_t aStartFrame) {
+ // We should be advanced right up to the last decoded frame.
+ EXPECT_TRUE(aQueue.MayDiscard());
+ EXPECT_FALSE(aQueue.SizeKnown());
+ EXPECT_EQ(aBatch, aQueue.Batch());
+ EXPECT_EQ(aThreshold, aQueue.PendingInsert());
+ EXPECT_EQ(aThreshold, aQueue.Size());
+ EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
+ EXPECT_EQ(size_t(1), aQueue.Display().size());
+ EXPECT_EQ(size_t(4), aQueue.PendingDecode());
+ VerifyDiscardingQueueContents(aQueue);
+
+ // Reset should clear everything except the first frame.
+ VerifyReset(aQueue, false, aFirstFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingReset) {
+ const size_t kThreshold = 8;
+ const size_t kBatch = 3;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, ResetBeforeDiscardingThreshold) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // Get the starting buffer to just before the point where we need to switch
+ // to a discarding buffer, reset the animation so advancing points at the
+ // first frame, and insert the last frame to cross the threshold.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ bool restartDecoder = retained.Reset();
+ EXPECT_FALSE(restartDecoder);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ EXPECT_TRUE(firstFrame != nullptr);
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ const imgFrame* displayFirstFrame = buffer.Get(0, true);
+ const imgFrame* advanceFirstFrame = buffer.Get(0, false);
+ EXPECT_EQ(firstFrame, displayFirstFrame);
+ EXPECT_EQ(firstFrame, advanceFirstFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingTooFewFrames) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // First get us to a discarding buffer state.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ // Insert one more frame.
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ VerifyAdvance(buffer, 2, true);
+ VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Mark it as complete.
+ bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Insert one fewer frame than before.
+ VerifyAdvance(buffer, 3, true);
+ VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // When we mark it as complete, it should fail due to too few frames.
+ restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_TRUE(buffer.HasRedecodeError());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, DiscardingTooManyFrames) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+
+ // First get us to a discarding buffer state.
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
+ VerifyInsertAndAdvance(retained, 1,
+ AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);
+
+ // Insert one more frame.
+ AnimationFrameDiscardingQueue buffer(std::move(retained));
+ VerifyAdvance(buffer, 2, true);
+ VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Mark it as complete.
+ bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
+ EXPECT_FALSE(restartDecoder);
+ EXPECT_FALSE(buffer.HasRedecodeError());
+
+ // Advance and insert to get us back to the end on the redecode.
+ VerifyAdvance(buffer, 3, true);
+ VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);
+ VerifyInsertAndAdvance(buffer, 3, AnimationFrameBuffer::InsertStatus::YIELD);
+
+ // Attempt to insert a 5th frame, it should fail.
+ RefPtr<imgFrame> frame = CreateEmptyFrame();
+ AnimationFrameBuffer::InsertStatus status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+ EXPECT_TRUE(buffer.HasRedecodeError());
+ EXPECT_EQ(size_t(0), buffer.PendingDecode());
+ EXPECT_EQ(size_t(4), buffer.Size());
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingReset) {
+ const size_t kThreshold = 8;
+ const size_t kBatch = 3;
+ const size_t kStartFrame = 0;
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+ PrepareForDiscardingQueue(retained);
+ const imgFrame* firstFrame = retained.Frames()[0].get();
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+ TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingResetBeforeComplete) {
+ const size_t kThreshold = 3;
+ const size_t kBatch = 1;
+ const size_t kStartFrame = 0;
+ const gfx::IntSize kImageSize(100, 100);
+ const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+
+ // Get the starting buffer to just before the point where we need to switch
+ // to a discarding buffer, reset the animation so advancing points at the
+ // first frame, and insert the last frame to cross the threshold.
+ RefPtr<imgFrame> frame;
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
+ false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(retained, 1, true);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(20, 10), gfx::IntSize(1, 1)),
+ false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_YIELD, status);
+
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+ bool restartDecoding = buffer.Reset();
+ EXPECT_TRUE(restartDecoding);
+
+ // None of the buffers were recyclable.
+ EXPECT_FALSE(buffer.Recycle().empty());
+ while (!buffer.Recycle().empty()) {
+ gfx::IntRect recycleRect;
+ RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ }
+
+ // Reinsert the first two frames as recyclable and reset again.
+ frame = CreateEmptyFrame(kImageSize, kImageRect, true);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(
+ kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
+ true);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ restartDecoding = buffer.Reset();
+ EXPECT_TRUE(restartDecoding);
+
+ // Now both buffers should have been saved and the dirty rect replaced with
+ // the full image rect since we don't know the first frame refresh area yet.
+ EXPECT_EQ(size_t(2), buffer.Recycle().size());
+ for (const auto& entry : buffer.Recycle()) {
+ EXPECT_EQ(kImageRect, entry.mDirtyRect);
+ }
+}
+
+TEST_F(ImageAnimationFrameBuffer, RecyclingRect) {
+ const size_t kThreshold = 5;
+ const size_t kBatch = 2;
+ const size_t kStartFrame = 0;
+ const gfx::IntSize kImageSize(100, 100);
+ const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
+ AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
+
+ // Let's get to the recycling state while marking all of the frames as not
+ // recyclable, just like AnimationFrameBuffer / the decoders would do.
+ RefPtr<imgFrame> frame;
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(retained, 1, false);
+ VerifyAdvance(retained, 2, true);
+ VerifyAdvance(retained, 3, false);
+
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = retained.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE, status);
+
+ AnimationFrameRecyclingQueue buffer(std::move(retained));
+
+ // The first frame is now the candidate for recycling. Since it was marked as
+ // not recyclable, we should get nothing.
+ VerifyAdvance(buffer, 4, false);
+
+ gfx::IntRect recycleRect;
+ EXPECT_FALSE(buffer.Recycle().empty());
+ RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert a recyclable partial frame. Its dirty rect shouldn't matter since
+ // the previous frame was not recyclable.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(0, 0, 25, 25));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 5, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_FALSE(ReinitForRecycle(frameRef));
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert a recyclable partial frame. Its dirty rect should match the recycle
+ // rect since it is the only frame in the buffer.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(25, 0, 50, 50));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 6, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(25, 0, 50, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Insert the last frame and mark us as complete. The next recycled frame is
+ // producing the first frame again, so we should use the first frame refresh
+ // area instead of its dirty rect.
+ frame = CreateEmptyFrame(kImageSize, gfx::IntRect(10, 10, 60, 10));
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ bool continueDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 75, 50));
+ EXPECT_FALSE(continueDecoding);
+
+ VerifyAdvance(buffer, 7, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+
+ // Now let's reinsert the first frame. The recycle rect should still be the
+ // first frame refresh area instead of the dirty rect of the first frame (e.g.
+ // the full frame).
+ frame = CreateEmptyFrame(kImageSize, kImageRect, false);
+ status = buffer.Insert(std::move(frame));
+ EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
+
+ VerifyAdvance(buffer, 0, true);
+ EXPECT_FALSE(buffer.Recycle().empty());
+ frameRef = buffer.RecycleFrame(recycleRect);
+ EXPECT_TRUE(frameRef);
+ EXPECT_TRUE(ReinitForRecycle(frameRef));
+ EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
+ EXPECT_TRUE(buffer.Recycle().empty());
+}