Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "Decoder.h"
#include "DecodePool.h"
#include "IDecodingTask.h"
#include "ISurfaceProvider.h"
#include "gfxPlatform.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/Point.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/Telemetry.h"
#include "nsComponentManagerUtils.h"
#include "nsProxyRelease.h"
#include "nsServiceManagerUtils.h"
using mozilla::gfx::IntPoint;
using mozilla::gfx::IntRect;
using mozilla::gfx::IntSize;
using mozilla::gfx::SurfaceFormat;
namespace mozilla {
namespace image {
class MOZ_STACK_CLASS AutoRecordDecoderTelemetry final {
public:
explicit AutoRecordDecoderTelemetry(Decoder* aDecoder) : mDecoder(aDecoder) {
MOZ_ASSERT(mDecoder);
// Begin recording telemetry data.
mStartTime = TimeStamp::Now();
}
~AutoRecordDecoderTelemetry() {
// Finish telemetry.
mDecoder->mDecodeTime += (TimeStamp::Now() - mStartTime);
}
private:
Decoder* mDecoder;
TimeStamp mStartTime;
};
Decoder::Decoder(RasterImage* aImage)
: mInProfile(nullptr),
mTransform(nullptr),
mImageData(nullptr),
mImageDataLength(0),
mCMSMode(gfxPlatform::GetCMSMode()),
mImage(aImage),
mFrameRecycler(nullptr),
mProgress(NoProgress),
mFrameCount(0),
mLoopLength(FrameTimeout::Zero()),
mDecoderFlags(DefaultDecoderFlags()),
mSurfaceFlags(DefaultSurfaceFlags()),
mInitialized(false),
mMetadataDecode(false),
mHaveExplicitOutputSize(false),
mInFrame(false),
mFinishedNewFrame(false),
mHasFrameToTake(false),
mReachedTerminalState(false),
mDecodeDone(false),
mError(false),
mShouldReportError(false),
mFinalizeFrames(true) {}
Decoder::~Decoder() {
MOZ_ASSERT(mProgress == NoProgress || !mImage,
"Destroying Decoder without taking all its progress changes");
MOZ_ASSERT(mInvalidRect.IsEmpty() || !mImage,
"Destroying Decoder without taking all its invalidations");
mInitialized = false;
if (mInProfile) {
// mTransform belongs to us only if mInProfile is non-null
if (mTransform) {
qcms_transform_release(mTransform);
}
qcms_profile_release(mInProfile);
}
if (mImage && !NS_IsMainThread()) {
// Dispatch mImage to main thread to prevent it from being destructed by the
// decode thread.
SurfaceCache::ReleaseImageOnMainThread(mImage.forget());
}
}
void Decoder::SetSurfaceFlags(SurfaceFlags aSurfaceFlags) {
MOZ_ASSERT(!mInitialized);
MOZ_ASSERT(!(mSurfaceFlags & SurfaceFlags::NO_COLORSPACE_CONVERSION) ||
!(mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE));
mSurfaceFlags = aSurfaceFlags;
if (mSurfaceFlags & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
mCMSMode = CMSMode::Off;
}
if (mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) {
// CMSMode::TaggedOnly and CMSMode::All are equivalent when the
// TO_SRGB_COLORSPACE flag is set (for untagged images CMSMode::All assumes
// they are in sRGB space so it does nothing, which is same as what
// CMSMode::TaggedOnly does for untagged images). We just want to avoid
// CMSMode::Off so that the sRGB conversion actually happens.
mCMSMode = CMSMode::All;
}
}
qcms_profile* Decoder::GetCMSOutputProfile() const {
if (mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) {
return gfxPlatform::GetCMSsRGBProfile();
}
return gfxPlatform::GetCMSOutputProfile();
}
qcms_transform* Decoder::GetCMSsRGBTransform(SurfaceFormat aFormat) const {
if (mSurfaceFlags & SurfaceFlags::TO_SRGB_COLORSPACE) {
// We want a transform to convert from sRGB to device space, but we are
// already using sRGB as our device space. That means we can skip
// color management entirely.
return nullptr;
}
if (qcms_profile_is_sRGB(gfxPlatform::GetCMSOutputProfile())) {
// Device space is sRGB so we can skip color management as well.
return nullptr;
}
switch (aFormat) {
case SurfaceFormat::B8G8R8A8:
case SurfaceFormat::B8G8R8X8:
return gfxPlatform::GetCMSBGRATransform();
case SurfaceFormat::R8G8B8A8:
case SurfaceFormat::R8G8B8X8:
return gfxPlatform::GetCMSRGBATransform();
case SurfaceFormat::R8G8B8:
return gfxPlatform::GetCMSRGBTransform();
default:
MOZ_ASSERT_UNREACHABLE("Unsupported surface format!");
return nullptr;
}
}
/*
* Common implementation of the decoder interface.
*/
nsresult Decoder::Init() {
// No re-initializing
MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!");
// All decoders must have a SourceBufferIterator.
MOZ_ASSERT(mIterator);
// Metadata decoders must not set an output size.
MOZ_ASSERT_IF(mMetadataDecode, !mHaveExplicitOutputSize);
// All decoders must be anonymous except for metadata decoders.
// XXX(seth): Soon that exception will be removed.
MOZ_ASSERT_IF(mImage, IsMetadataDecode());
// We can only request the frame count for metadata decoders.
MOZ_ASSERT_IF(WantsFrameCount(), IsMetadataDecode());
// Implementation-specific initialization.
nsresult rv = InitInternal();
mInitialized = true;
return rv;
}
LexerResult Decoder::Decode(IResumable* aOnResume /* = nullptr */) {
MOZ_ASSERT(mInitialized, "Should be initialized here");
MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator");
// If we're already done, don't attempt to keep decoding.
if (GetDecodeDone()) {
return LexerResult(HasError() ? TerminalState::FAILURE
: TerminalState::SUCCESS);
}
LexerResult lexerResult(TerminalState::FAILURE);
{
AUTO_PROFILER_LABEL_CATEGORY_PAIR_RELEVANT_FOR_JS(GRAPHICS_ImageDecoding);
AutoRecordDecoderTelemetry telemetry(this);
lexerResult = DoDecode(*mIterator, aOnResume);
};
if (lexerResult.is<Yield>()) {
// We either need more data to continue (in which case either @aOnResume or
// the caller will reschedule us to run again later), or the decoder is
// yielding to allow the caller access to some intermediate output.
return lexerResult;
}
// We reached a terminal state; we're now done decoding.
MOZ_ASSERT(lexerResult.is<TerminalState>());
mReachedTerminalState = true;
// If decoding failed, record that fact.
if (lexerResult.as<TerminalState>() == TerminalState::FAILURE) {
PostError();
}
// Perform final cleanup.
CompleteDecode();
return LexerResult(HasError() ? TerminalState::FAILURE
: TerminalState::SUCCESS);
}
LexerResult Decoder::TerminateFailure() {
PostError();
// Perform final cleanup if need be.
if (!mReachedTerminalState) {
mReachedTerminalState = true;
CompleteDecode();
}
return LexerResult(TerminalState::FAILURE);
}
bool Decoder::ShouldSyncDecode(size_t aByteLimit) {
MOZ_ASSERT(aByteLimit > 0);
MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator");
return mIterator->RemainingBytesIsNoMoreThan(aByteLimit);
}
void Decoder::CompleteDecode() {
// Implementation-specific finalization.
nsresult rv = BeforeFinishInternal();
if (NS_FAILED(rv)) {
PostError();
}
rv = HasError() ? FinishWithErrorInternal() : FinishInternal();
if (NS_FAILED(rv)) {
PostError();
}
if (IsMetadataDecode()) {
// If this was a metadata decode and we never got a size, the decode failed.
if (!HasSize()) {
PostError();
}
return;
}
// If the implementation left us mid-frame, finish that up. Note that it may
// have left us transparent.
if (mInFrame) {
PostHasTransparency();
PostFrameStop();
}
// If PostDecodeDone() has not been called, we may need to send teardown
// notifications if it is unrecoverable.
if (mDecodeDone) {
MOZ_ASSERT(HasError() || mCurrentFrame, "Should have an error or a frame");
} else {
// We should always report an error to the console in this case.
mShouldReportError = true;
if (GetCompleteFrameCount() > 0) {
// We're usable if we have at least one complete frame, so do exactly
// what we should have when the decoder completed.
PostHasTransparency();
PostDecodeDone();
} else {
// We're not usable. Record some final progress indicating the error.
mProgress |= FLAG_DECODE_COMPLETE | FLAG_HAS_ERROR;
}
}
}
void Decoder::SetOutputSize(const OrientedIntSize& aSize) {
mOutputSize = Some(aSize);
mHaveExplicitOutputSize = true;
}
Maybe<OrientedIntSize> Decoder::ExplicitOutputSize() const {
MOZ_ASSERT_IF(mHaveExplicitOutputSize, mOutputSize);
return mHaveExplicitOutputSize ? mOutputSize : Nothing();
}
Maybe<uint32_t> Decoder::TakeCompleteFrameCount() {
const bool finishedNewFrame = mFinishedNewFrame;
mFinishedNewFrame = false;
return finishedNewFrame ? Some(GetCompleteFrameCount()) : Nothing();
}
DecoderFinalStatus Decoder::FinalStatus() const {
return DecoderFinalStatus(IsMetadataDecode(), GetDecodeDone(), HasError(),
ShouldReportError());
}
DecoderTelemetry Decoder::Telemetry() const {
MOZ_ASSERT(mIterator);
return DecoderTelemetry(SpeedHistogram(),
mIterator ? mIterator->ByteCount() : 0,
mIterator ? mIterator->ChunkCount() : 0, mDecodeTime);
}
nsresult Decoder::AllocateFrame(const gfx::IntSize& aOutputSize,
gfx::SurfaceFormat aFormat,
const Maybe<AnimationParams>& aAnimParams) {
mCurrentFrame = AllocateFrameInternal(aOutputSize, aFormat, aAnimParams,
std::move(mCurrentFrame));
if (mCurrentFrame) {
mHasFrameToTake = true;
mImageData = mCurrentFrame.Data();
// We should now be on |aFrameNum|. (Note that we're comparing the frame
// number, which is zero-based, with the frame count, which is one-based.)
MOZ_ASSERT_IF(aAnimParams, aAnimParams->mFrameNum + 1 == mFrameCount);
// If we're past the first frame, PostIsAnimated() should've been called.
MOZ_ASSERT_IF(mFrameCount > 1, HasAnimation());
// Update our state to reflect the new frame.
MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!");
mInFrame = true;
} else {
mImageData = nullptr;
mImageDataLength = 0;
}
return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE;
}
RawAccessFrameRef Decoder::AllocateFrameInternal(
const gfx::IntSize& aOutputSize, SurfaceFormat aFormat,
const Maybe<AnimationParams>& aAnimParams,
RawAccessFrameRef&& aPreviousFrame) {
if (HasError()) {
return RawAccessFrameRef();
}
uint32_t frameNum = aAnimParams ? aAnimParams->mFrameNum : 0;
if (frameNum != mFrameCount) {
MOZ_ASSERT_UNREACHABLE("Allocating frames out of order");
return RawAccessFrameRef();
}
if (aOutputSize.width <= 0 || aOutputSize.height <= 0) {
NS_WARNING("Trying to add frame with zero or negative size");
return RawAccessFrameRef();
}
if (frameNum > 0) {
if (aPreviousFrame->GetDisposalMethod() !=
DisposalMethod::RESTORE_PREVIOUS) {
// If the new restore frame is the direct previous frame, then we know
// the dirty rect is composed only of the current frame's blend rect and
// the restore frame's clear rect (if applicable) which are handled in
// filters.
mRestoreFrame = std::move(aPreviousFrame);
mRestoreDirtyRect.SetBox(0, 0, 0, 0);
} else {
// We only need the previous frame's dirty rect, because while there may
// have been several frames between us and mRestoreFrame, the only areas
// that changed are the restore frame's clear rect, the current frame
// blending rect, and the previous frame's blending rect. All else is
// forgotten due to us restoring the same frame again.
mRestoreDirtyRect = aPreviousFrame->GetBoundedBlendRect();
}
}
RawAccessFrameRef ref;
// If we have a frame recycler, it must be for an animated image producing
// full frames. If the higher layers are discarding frames because of the
// memory footprint, then the recycler will allow us to reuse the buffers.
// Each frame should be the same size and have mostly the same properties.
if (mFrameRecycler) {
MOZ_ASSERT(aAnimParams);
ref = mFrameRecycler->RecycleFrame(mRecycleRect);
if (ref) {
// If the recycled frame is actually the current restore frame, we cannot
// use it. If the next restore frame is the new frame we are creating, in
// theory we could reuse it, but we would need to store the restore frame
// animation parameters elsewhere. For now we just drop it.
bool blocked = ref.get() == mRestoreFrame.get();
if (!blocked) {
blocked = NS_FAILED(
ref->InitForDecoderRecycle(aAnimParams.ref(), &mImageDataLength));
}
if (blocked) {
ref.reset();
}
}
}
// Either the recycler had nothing to give us, or we don't have a recycler.
// Produce a new frame to store the data.
if (!ref) {
// There is no underlying data to reuse, so reset the recycle rect to be
// the full frame, to ensure the restore frame is fully copied.
mRecycleRect = IntRect(IntPoint(0, 0), aOutputSize);
bool nonPremult = bool(mSurfaceFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
auto frame = MakeNotNull<RefPtr<imgFrame>>();
if (NS_FAILED(frame->InitForDecoder(aOutputSize, aFormat, nonPremult,
aAnimParams, bool(mFrameRecycler),
&mImageDataLength))) {
NS_WARNING("imgFrame::Init should succeed");
return RawAccessFrameRef();
}
ref = frame->RawAccessRef(gfx::DataSourceSurface::READ_WRITE);
if (!ref) {
frame->Abort();
return RawAccessFrameRef();
}
}
mFrameCount++;
return ref;
}
/*
* Hook stubs. Override these as necessary in decoder implementations.
*/
nsresult Decoder::InitInternal() { return NS_OK; }
nsresult Decoder::BeforeFinishInternal() { return NS_OK; }
nsresult Decoder::FinishInternal() { return NS_OK; }
nsresult Decoder::FinishWithErrorInternal() {
MOZ_ASSERT(!mInFrame);
return NS_OK;
}
/*
* Progress Notifications
*/
void Decoder::PostSize(int32_t aWidth, int32_t aHeight,
Orientation aOrientation, Resolution aResolution) {
// Validate.
MOZ_ASSERT(aWidth >= 0, "Width can't be negative!");
MOZ_ASSERT(aHeight >= 0, "Height can't be negative!");
// Set our intrinsic size.
mImageMetadata.SetSize(aWidth, aHeight, aOrientation, aResolution);
// Verify it is the expected size, if given. Note that this is only used by
// the ICO decoder for embedded image types, so only its subdecoders are
// required to handle failures in PostSize.
if (!IsExpectedSize()) {
PostError();
return;
}
// Set our output size if it's not already set.
if (!mOutputSize) {
mOutputSize = Some(mImageMetadata.GetSize());
}
MOZ_ASSERT(mOutputSize->width <= mImageMetadata.GetSize().width &&
mOutputSize->height <= mImageMetadata.GetSize().height,
"Output size will result in upscaling");
// Record this notification.
mProgress |= FLAG_SIZE_AVAILABLE;
}
void Decoder::PostHasTransparency() { mProgress |= FLAG_HAS_TRANSPARENCY; }
void Decoder::PostIsAnimated(FrameTimeout aFirstFrameTimeout) {
mProgress |= FLAG_IS_ANIMATED;
mImageMetadata.SetHasAnimation();
mImageMetadata.SetFirstFrameTimeout(aFirstFrameTimeout);
}
void Decoder::PostFrameCount(uint32_t aFrameCount) {
mImageMetadata.SetFrameCount(aFrameCount);
}
void Decoder::PostFrameStop(Opacity aFrameOpacity) {
// 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.
mInFrame = false;
mFinishedNewFrame = true;
mCurrentFrame->Finish(
aFrameOpacity, mFinalizeFrames,
/* aOrientationSwapsWidthAndHeight = */ mImageMetadata.HasOrientation() &&
mImageMetadata.GetOrientation().SwapsWidthAndHeight());
mProgress |= FLAG_FRAME_COMPLETE;
mLoopLength += mCurrentFrame->GetTimeout();
if (mFrameCount == 1) {
// If we're not sending partial invalidations, then we send an invalidation
// here when the first frame is complete.
if (!ShouldSendPartialInvalidations()) {
mInvalidRect.UnionRect(mInvalidRect,
OrientedIntRect(OrientedIntPoint(), Size()));
}
// If we dispose of the first frame by clearing it, then the first frame's
// refresh area is all of itself. RESTORE_PREVIOUS is invalid (assumed to
// be DISPOSE_CLEAR).
switch (mCurrentFrame->GetDisposalMethod()) {
default:
MOZ_FALLTHROUGH_ASSERT("Unexpected DisposalMethod");
case DisposalMethod::CLEAR:
case DisposalMethod::CLEAR_ALL:
case DisposalMethod::RESTORE_PREVIOUS:
mFirstFrameRefreshArea = IntRect(IntPoint(), Size().ToUnknownSize());
break;
case DisposalMethod::KEEP:
case DisposalMethod::NOT_SPECIFIED:
break;
}
} else {
// 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,
mCurrentFrame->GetBoundedBlendRect());
}
}
void Decoder::PostInvalidation(const OrientedIntRect& aRect,
const Maybe<OrientedIntRect>& aRectAtOutputSize
/* = Nothing() */) {
// We should be mid-frame
MOZ_ASSERT(mInFrame, "Can't invalidate when not mid-frame!");
MOZ_ASSERT(mCurrentFrame, "Can't invalidate when not mid-frame!");
// Record this invalidation, unless we're not sending partial invalidations
// or we're past the first frame.
if (ShouldSendPartialInvalidations() && mFrameCount == 1) {
mInvalidRect.UnionRect(mInvalidRect, aRect);
mCurrentFrame->ImageUpdated(
aRectAtOutputSize.valueOr(aRect).ToUnknownRect());
}
}
void Decoder::PostLoopCount(int32_t aLoopCount) {
mImageMetadata.SetLoopCount(aLoopCount);
}
void Decoder::PostDecodeDone() {
MOZ_ASSERT(!IsMetadataDecode(), "Done with decoding in metadata decode");
MOZ_ASSERT(!mInFrame, "Can't be done decoding if we're mid-frame!");
MOZ_ASSERT(!mDecodeDone, "Decode already done!");
mDecodeDone = true;
// Some metadata that we track should take into account every frame in the
// image. If this is a first-frame-only decode, our accumulated loop length
// and first frame refresh area only includes the first frame, so it's not
// correct and we don't record it.
if (!IsFirstFrameDecode()) {
mImageMetadata.SetLoopLength(mLoopLength);
mImageMetadata.SetFirstFrameRefreshArea(mFirstFrameRefreshArea);
}
mProgress |= FLAG_DECODE_COMPLETE;
}
void Decoder::PostError() {
mError = true;
if (mInFrame) {
MOZ_ASSERT(mCurrentFrame);
MOZ_ASSERT(mFrameCount > 0);
mCurrentFrame->Abort();
mInFrame = false;
--mFrameCount;
mHasFrameToTake = false;
}
}
} // namespace image
} // namespace mozilla