diff -r 000000000000 -r 6474c204b198 image/src/FrameBlender.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/image/src/FrameBlender.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,613 @@ +/* -*- 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 "FrameBlender.h" + +#include "mozilla/MemoryReporting.h" +#include "MainThreadUtils.h" + +#include "pixman.h" + +using namespace mozilla; +using namespace mozilla::image; + +namespace mozilla { +namespace image { + +FrameBlender::FrameBlender(FrameSequence* aSequenceToUse /* = nullptr */) + : mFrames(aSequenceToUse) + , mAnim(nullptr) + , mLoopCount(-1) +{ + if (!mFrames) { + mFrames = new FrameSequence(); + } +} + +FrameBlender::~FrameBlender() +{ + delete mAnim; +} + +already_AddRefed +FrameBlender::GetFrameSequence() +{ + nsRefPtr seq(mFrames); + return seq.forget(); +} + +imgFrame* +FrameBlender::GetFrame(uint32_t framenum) const +{ + if (!mAnim) { + NS_ASSERTION(framenum == 0, "Don't ask for a frame > 0 if we're not animated!"); + return mFrames->GetFrame(0); + } + if (mAnim->lastCompositedFrameIndex == int32_t(framenum)) + return mAnim->compositingFrame; + return mFrames->GetFrame(framenum); +} + +imgFrame* +FrameBlender::RawGetFrame(uint32_t framenum) const +{ + if (!mAnim) { + NS_ASSERTION(framenum == 0, "Don't ask for a frame > 0 if we're not animated!"); + return mFrames->GetFrame(0); + } + + return mFrames->GetFrame(framenum); +} + +uint32_t +FrameBlender::GetNumFrames() const +{ + return mFrames->GetNumFrames(); +} + +int32_t +FrameBlender::GetTimeoutForFrame(uint32_t framenum) const +{ + const int32_t timeout = RawGetFrame(framenum)->GetRawTimeout(); + // 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 207059. + // The behavior of recent IE and Opera versions seems to be: + // IE 6/Win: + // 10 - 50ms go 100ms + // >50ms go correct speed + // Opera 7 final/Win: + // 10ms goes 100ms + // >10ms go correct speed + // It seems that there are broken tools out there that set a 0ms or 10ms + // timeout when they really want a "default" one. So munge values in that + // range. + if (timeout >= 0 && timeout <= 10 && mLoopCount != 0) + return 100; + return timeout; +} + +void +FrameBlender::SetLoopCount(int32_t aLoopCount) +{ + mLoopCount = aLoopCount; +} + +int32_t +FrameBlender::GetLoopCount() const +{ + return mLoopCount; +} + +void +FrameBlender::RemoveFrame(uint32_t framenum) +{ + NS_ABORT_IF_FALSE(framenum < GetNumFrames(), "Deleting invalid frame!"); + + mFrames->RemoveFrame(framenum); +} + +void +FrameBlender::ClearFrames() +{ + // Forget our old frame sequence, letting whoever else has it deal with it. + mFrames = new FrameSequence(); +} + +void +FrameBlender::InsertFrame(uint32_t framenum, imgFrame* aFrame) +{ + NS_ABORT_IF_FALSE(framenum <= GetNumFrames(), "Inserting invalid frame!"); + mFrames->InsertFrame(framenum, aFrame); + if (GetNumFrames() > 1) { + EnsureAnimExists(); + } +} + +imgFrame* +FrameBlender::SwapFrame(uint32_t framenum, imgFrame* aFrame) +{ + NS_ABORT_IF_FALSE(framenum < GetNumFrames(), "Swapping invalid frame!"); + + imgFrame* ret; + + // Steal the imgFrame from wherever it's currently stored + if (mAnim && mAnim->lastCompositedFrameIndex == int32_t(framenum)) { + ret = mAnim->compositingFrame.Forget(); + mAnim->lastCompositedFrameIndex = -1; + nsAutoPtr toDelete(mFrames->SwapFrame(framenum, aFrame)); + } else { + ret = mFrames->SwapFrame(framenum, aFrame); + } + + return ret; +} + +void +FrameBlender::EnsureAnimExists() +{ + if (!mAnim) { + // Create the animation context + mAnim = new Anim(); + + // We should only get into this code path directly after we've created our + // second frame (hence we know we're animated). + MOZ_ASSERT(GetNumFrames() == 2); + } +} + +//****************************************************************************** +// DoBlend gets called when the timer for animation get fired and we have to +// update the composited frame of the animation. +bool +FrameBlender::DoBlend(nsIntRect* aDirtyRect, + uint32_t aPrevFrameIndex, + uint32_t aNextFrameIndex) +{ + if (!aDirtyRect) { + return false; + } + + const FrameDataPair& prevFrame = mFrames->GetFrame(aPrevFrameIndex); + const FrameDataPair& nextFrame = mFrames->GetFrame(aNextFrameIndex); + if (!prevFrame.HasFrameData() || !nextFrame.HasFrameData()) { + return false; + } + + int32_t prevFrameDisposalMethod = prevFrame->GetFrameDisposalMethod(); + if (prevFrameDisposalMethod == FrameBlender::kDisposeRestorePrevious && + !mAnim->compositingPrevFrame) + prevFrameDisposalMethod = FrameBlender::kDisposeClear; + + nsIntRect prevFrameRect = prevFrame->GetRect(); + bool isFullPrevFrame = (prevFrameRect.x == 0 && prevFrameRect.y == 0 && + prevFrameRect.width == mSize.width && + prevFrameRect.height == mSize.height); + + // Optimization: DisposeClearAll if the previous frame is the same size as + // container and it's clearing itself + if (isFullPrevFrame && + (prevFrameDisposalMethod == FrameBlender::kDisposeClear)) + prevFrameDisposalMethod = FrameBlender::kDisposeClearAll; + + int32_t nextFrameDisposalMethod = nextFrame->GetFrameDisposalMethod(); + nsIntRect nextFrameRect = nextFrame->GetRect(); + bool isFullNextFrame = (nextFrameRect.x == 0 && nextFrameRect.y == 0 && + nextFrameRect.width == mSize.width && + nextFrameRect.height == mSize.height); + + if (!nextFrame->GetIsPaletted()) { + // Optimization: Skip compositing if the previous frame wants to clear the + // whole image + if (prevFrameDisposalMethod == FrameBlender::kDisposeClearAll) { + aDirtyRect->SetRect(0, 0, mSize.width, mSize.height); + return true; + } + + // Optimization: Skip compositing if this frame is the same size as the + // container and it's fully drawing over prev frame (no alpha) + if (isFullNextFrame && + (nextFrameDisposalMethod != FrameBlender::kDisposeRestorePrevious) && + !nextFrame->GetHasAlpha()) { + aDirtyRect->SetRect(0, 0, mSize.width, mSize.height); + return true; + } + } + + // Calculate area that needs updating + switch (prevFrameDisposalMethod) { + default: + case FrameBlender::kDisposeNotSpecified: + case FrameBlender::kDisposeKeep: + *aDirtyRect = nextFrameRect; + break; + + case FrameBlender::kDisposeClearAll: + // Whole image container is cleared + aDirtyRect->SetRect(0, 0, mSize.width, mSize.height); + break; + + case FrameBlender::kDisposeClear: + // Calc area that needs to be redrawn (the combination of previous and + // this frame) + // XXX - This could be done with multiple framechanged calls + // Having prevFrame way at the top of the image, and nextFrame + // way at the bottom, and both frames being small, we'd be + // telling framechanged to refresh the whole image when only two + // small areas are needed. + aDirtyRect->UnionRect(nextFrameRect, prevFrameRect); + break; + + case FrameBlender::kDisposeRestorePrevious: + aDirtyRect->SetRect(0, 0, mSize.width, mSize.height); + break; + } + + // 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 (mAnim->lastCompositedFrameIndex == int32_t(aNextFrameIndex)) { + return true; + } + + bool needToBlankComposite = false; + + // Create the Compositing Frame + if (!mAnim->compositingFrame) { + mAnim->compositingFrame.SetFrame(new imgFrame()); + nsresult rv = mAnim->compositingFrame->Init(0, 0, mSize.width, mSize.height, + gfxImageFormat::ARGB32); + if (NS_FAILED(rv)) { + mAnim->compositingFrame.SetFrame(nullptr); + return false; + } + mAnim->compositingFrame.LockAndGetData(); + needToBlankComposite = true; + } else if (int32_t(aNextFrameIndex) != mAnim->lastCompositedFrameIndex+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; + } + + // More optimizations possible when next frame is not transparent + // But if the next frame has FrameBlender::kDisposeRestorePrevious, + // this "no disposal" optimization is not possible, + // because the frame in "after disposal operation" state + // needs to be stored in compositingFrame, so it can be + // copied into compositingPrevFrame later. + bool doDisposal = true; + if (!nextFrame->GetHasAlpha() && + nextFrameDisposalMethod != FrameBlender::kDisposeRestorePrevious) { + if (isFullNextFrame) { + // Optimization: No need to dispose prev.frame when + // next frame is full frame and not transparent. + doDisposal = false; + // No need to blank the composite frame + needToBlankComposite = false; + } else { + if ((prevFrameRect.x >= nextFrameRect.x) && + (prevFrameRect.y >= nextFrameRect.y) && + (prevFrameRect.x + prevFrameRect.width <= nextFrameRect.x + nextFrameRect.width) && + (prevFrameRect.y + prevFrameRect.height <= nextFrameRect.y + nextFrameRect.height)) { + // Optimization: No need to dispose prev.frame when + // next frame fully overlaps previous frame. + doDisposal = false; + } + } + } + + if (doDisposal) { + // Dispose of previous: clear, restore, or keep (copy) + switch (prevFrameDisposalMethod) { + case FrameBlender::kDisposeClear: + if (needToBlankComposite) { + // If we just created the composite, it could have anything in its + // buffer. Clear whole frame + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + } else { + // Only blank out previous frame area (both color & Mask/Alpha) + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect(), + prevFrameRect); + } + break; + + case FrameBlender::kDisposeClearAll: + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + break; + + case FrameBlender::kDisposeRestorePrevious: + // It would be better to copy only the area changed back to + // compositingFrame. + if (mAnim->compositingPrevFrame) { + CopyFrameImage(mAnim->compositingPrevFrame.GetFrameData(), + mAnim->compositingPrevFrame.GetFrame()->GetRect(), + mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + + // destroy only if we don't need it for this frame's disposal + if (nextFrameDisposalMethod != FrameBlender::kDisposeRestorePrevious) + mAnim->compositingPrevFrame.SetFrame(nullptr); + } else { + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + } + break; + + default: + // 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 (mAnim->lastCompositedFrameIndex != int32_t(aNextFrameIndex - 1)) { + if (isFullPrevFrame && !prevFrame->GetIsPaletted()) { + // Just copy the bits + CopyFrameImage(prevFrame.GetFrameData(), + prevFrame.GetFrame()->GetRect(), + mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + } else { + if (needToBlankComposite) { + // Only blank composite when prev is transparent or not full. + if (prevFrame->GetHasAlpha() || !isFullPrevFrame) { + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + } + } + DrawFrameTo(prevFrame.GetFrameData(), prevFrameRect, + prevFrame.GetFrame()->PaletteDataLength(), + prevFrame.GetFrame()->GetHasAlpha(), + mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect(), + FrameBlendMethod(prevFrame.GetFrame()->GetBlendMethod())); + } + } + } + } else if (needToBlankComposite) { + // If we just created the composite, it could have anything in it's + // buffers. Clear them + ClearFrame(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect()); + } + + // Check if the frame we are composing wants the previous image restored afer + // it is done. Don't store it (again) if last frame wanted its image restored + // too + if ((nextFrameDisposalMethod == FrameBlender::kDisposeRestorePrevious) && + (prevFrameDisposalMethod != FrameBlender::kDisposeRestorePrevious)) { + // We are storing the whole image. + // It would be better if we just stored the area that nextFrame is going to + // overwrite. + if (!mAnim->compositingPrevFrame) { + mAnim->compositingPrevFrame.SetFrame(new imgFrame()); + nsresult rv = mAnim->compositingPrevFrame->Init(0, 0, mSize.width, mSize.height, + gfxImageFormat::ARGB32); + if (NS_FAILED(rv)) { + mAnim->compositingPrevFrame.SetFrame(nullptr); + return false; + } + + mAnim->compositingPrevFrame.LockAndGetData(); + } + + CopyFrameImage(mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect(), + mAnim->compositingPrevFrame.GetFrameData(), + mAnim->compositingPrevFrame.GetFrame()->GetRect()); + } + + // blit next frame into it's correct spot + DrawFrameTo(nextFrame.GetFrameData(), nextFrameRect, + nextFrame.GetFrame()->PaletteDataLength(), + nextFrame.GetFrame()->GetHasAlpha(), + mAnim->compositingFrame.GetFrameData(), + mAnim->compositingFrame.GetFrame()->GetRect(), + FrameBlendMethod(nextFrame.GetFrame()->GetBlendMethod())); + + // Set timeout of CompositeFrame to timeout of frame we just composed + // Bug 177948 + int32_t timeout = nextFrame->GetRawTimeout(); + mAnim->compositingFrame->SetRawTimeout(timeout); + + // Tell the image that it is fully 'downloaded'. + nsresult rv = mAnim->compositingFrame->ImageUpdated(mAnim->compositingFrame->GetRect()); + if (NS_FAILED(rv)) { + return false; + } + + mAnim->lastCompositedFrameIndex = int32_t(aNextFrameIndex); + + return true; +} + +//****************************************************************************** +// Fill aFrame with black. Does also clears the mask. +void +FrameBlender::ClearFrame(uint8_t* aFrameData, const nsIntRect& aFrameRect) +{ + if (!aFrameData) + return; + + memset(aFrameData, 0, aFrameRect.width * aFrameRect.height * 4); +} + +//****************************************************************************** +void +FrameBlender::ClearFrame(uint8_t* aFrameData, const nsIntRect& aFrameRect, const nsIntRect& aRectToClear) +{ + if (!aFrameData || aFrameRect.width <= 0 || aFrameRect.height <= 0 || + aRectToClear.width <= 0 || aRectToClear.height <= 0) { + return; + } + + nsIntRect toClear = aFrameRect.Intersect(aRectToClear); + if (toClear.IsEmpty()) { + return; + } + + uint32_t bytesPerRow = aFrameRect.width * 4; + for (int row = toClear.y; row < toClear.y + toClear.height; ++row) { + memset(aFrameData + toClear.x * 4 + row * bytesPerRow, 0, toClear.width * 4); + } +} + +//****************************************************************************** +// Whether we succeed or fail will not cause a crash, and there's not much +// we can do about a failure, so there we don't return a nsresult +bool +FrameBlender::CopyFrameImage(const uint8_t *aDataSrc, const nsIntRect& aRectSrc, + uint8_t *aDataDest, const nsIntRect& aRectDest) +{ + uint32_t dataLengthSrc = aRectSrc.width * aRectSrc.height * 4; + uint32_t dataLengthDest = aRectDest.width * aRectDest.height * 4; + + if (!aDataDest || !aDataSrc || dataLengthSrc != dataLengthDest) { + return false; + } + + memcpy(aDataDest, aDataSrc, dataLengthDest); + + return true; +} + +nsresult +FrameBlender::DrawFrameTo(const uint8_t *aSrcData, const nsIntRect& aSrcRect, + uint32_t aSrcPaletteLength, bool aSrcHasAlpha, + uint8_t *aDstPixels, const nsIntRect& aDstRect, + FrameBlender::FrameBlendMethod aBlendMethod) +{ + NS_ENSURE_ARG_POINTER(aSrcData); + NS_ENSURE_ARG_POINTER(aDstPixels); + + // According to both AGIF and APNG specs, offsets are unsigned + if (aSrcRect.x < 0 || aSrcRect.y < 0) { + NS_WARNING("FrameBlender::DrawFrameTo: negative offsets not allowed"); + return NS_ERROR_FAILURE; + } + // Outside the destination frame, skip it + if ((aSrcRect.x > aDstRect.width) || (aSrcRect.y > aDstRect.height)) { + return NS_OK; + } + + if (aSrcPaletteLength) { + // Larger than the destination frame, clip it + int32_t width = std::min(aSrcRect.width, aDstRect.width - aSrcRect.x); + int32_t height = std::min(aSrcRect.height, aDstRect.height - aSrcRect.y); + + // The clipped image must now fully fit within destination image frame + NS_ASSERTION((aSrcRect.x >= 0) && (aSrcRect.y >= 0) && + (aSrcRect.x + width <= aDstRect.width) && + (aSrcRect.y + height <= aDstRect.height), + "FrameBlender::DrawFrameTo: Invalid aSrcRect"); + + // clipped image size may be smaller than source, but not larger + NS_ASSERTION((width <= aSrcRect.width) && (height <= aSrcRect.height), + "FrameBlender::DrawFrameTo: source must be smaller than dest"); + + // Get pointers to image data + const uint8_t *srcPixels = aSrcData + aSrcPaletteLength; + uint32_t *dstPixels = reinterpret_cast(aDstPixels); + const uint32_t *colormap = reinterpret_cast(aSrcData); + + // Skip to the right offset + dstPixels += aSrcRect.x + (aSrcRect.y * aDstRect.width); + if (!aSrcHasAlpha) { + for (int32_t r = height; r > 0; --r) { + for (int32_t c = 0; c < width; c++) { + dstPixels[c] = colormap[srcPixels[c]]; + } + // Go to the next row in the source resp. destination image + srcPixels += aSrcRect.width; + dstPixels += aDstRect.width; + } + } else { + for (int32_t r = height; r > 0; --r) { + for (int32_t c = 0; c < width; c++) { + const uint32_t color = colormap[srcPixels[c]]; + if (color) + dstPixels[c] = color; + } + // Go to the next row in the source resp. destination image + srcPixels += aSrcRect.width; + dstPixels += aDstRect.width; + } + } + } else { + pixman_image_t* src = pixman_image_create_bits(aSrcHasAlpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, + aSrcRect.width, + aSrcRect.height, + reinterpret_cast(const_cast(aSrcData)), + aSrcRect.width * 4); + pixman_image_t* dst = pixman_image_create_bits(PIXMAN_a8r8g8b8, + aDstRect.width, + aDstRect.height, + reinterpret_cast(aDstPixels), + aDstRect.width * 4); + + pixman_image_composite32(aBlendMethod == FrameBlender::kBlendSource ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, + src, + nullptr, + dst, + 0, 0, + 0, 0, + aSrcRect.x, aSrcRect.y, + aSrcRect.width, aSrcRect.height); + + pixman_image_unref(src); + pixman_image_unref(dst); + } + + return NS_OK; +} + +void +FrameBlender::Discard() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // As soon as an image becomes animated, it becomes non-discardable and any + // timers are cancelled. + NS_ABORT_IF_FALSE(!mAnim, "Asked to discard for animated image!"); + + // Delete all the decoded frames, then clear the array. + ClearFrames(); +} + +size_t +FrameBlender::SizeOfDecodedWithComputedFallbackIfHeap(gfxMemoryLocation aLocation, + MallocSizeOf aMallocSizeOf) const +{ + size_t n = mFrames->SizeOfDecodedWithComputedFallbackIfHeap(aLocation, aMallocSizeOf); + + if (mAnim) { + if (mAnim->compositingFrame) { + n += mAnim->compositingFrame->SizeOfExcludingThisWithComputedFallbackIfHeap(aLocation, aMallocSizeOf); + } + if (mAnim->compositingPrevFrame) { + n += mAnim->compositingPrevFrame->SizeOfExcludingThisWithComputedFallbackIfHeap(aLocation, aMallocSizeOf); + } + } + + return n; +} + +void +FrameBlender::ResetAnimation() +{ + if (mAnim) { + mAnim->lastCompositedFrameIndex = -1; + } +} + +} // namespace image +} // namespace mozilla