Backed out 5 changesets (bug 1146663) for crashes a=backout
authorWes Kocher <wkocher@mozilla.com>
Mon, 21 Sep 2015 13:16:11 -0700
changeset 296114 968702bfd84b2c01ecee6591e0ebcb195e7afff1
parent 296113 3e5c15ad88940912747f2c5387441090113cf28d
child 296115 577c248da8de42756be578bc4f09301dbc5998b2
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1146663
milestone43.0a2
backs outc0d3f6e2a3e33f8b45bf4039126cbbe004448e3e
07d6fcc70e7c0476088f45264963505a63ebec53
77357a5a1ad1a0306b1f1f4688baf2d5312b315b
83a6dc9ee30c1c97314ea8a342154cba4a523ed1
2b6a3ec30a149c1142ac390fd5e5bf3e7a34a2e1
Backed out 5 changesets (bug 1146663) for crashes a=backout Backed out changeset c0d3f6e2a3e3 (bug 1146663) Backed out changeset 07d6fcc70e7c (bug 1146663) Backed out changeset 77357a5a1ad1 (bug 1146663) Backed out changeset 83a6dc9ee30c (bug 1146663) Backed out changeset 2b6a3ec30a14 (bug 1146663)
b2g/app/b2g.js
gfx/thebes/gfxPrefs.h
image/Decoder.cpp
image/Decoder.h
image/DecoderFactory.cpp
image/Image.h
image/ImageFactory.cpp
image/RasterImage.cpp
image/RasterImage.h
image/SurfaceCache.cpp
image/SurfaceCache.h
image/VectorImage.cpp
image/decoders/nsBMPDecoder.cpp
image/decoders/nsBMPDecoder.h
image/decoders/nsGIFDecoder2.cpp
image/decoders/nsGIFDecoder2.h
image/decoders/nsICODecoder.cpp
image/decoders/nsICODecoder.h
image/decoders/nsIconDecoder.cpp
image/decoders/nsIconDecoder.h
image/decoders/nsJPEGDecoder.cpp
image/decoders/nsJPEGDecoder.h
image/decoders/nsPNGDecoder.cpp
image/decoders/nsPNGDecoder.h
image/imgFrame.cpp
image/imgFrame.h
image/test/reftest/downscaling/reftest.list
layout/reftests/pixel-rounding/reftest.list
layout/tools/reftest/reftest-preferences.js
mobile/android/app/mobile.js
mobile/android/b2gdroid/app/b2gdroid.js
modules/libpref/init/all.js
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -57,16 +57,17 @@ pref("browser.cache.disk.smart_size.firs
 
 pref("browser.cache.memory.enable", true);
 pref("browser.cache.memory.capacity", 1024); // kilobytes
 
 pref("browser.cache.memory_limit", 2048); // 2 MB
 
 /* image cache prefs */
 pref("image.cache.size", 1048576); // bytes
+pref("image.high_quality_downscaling.enabled", false);
 pref("canvas.image.cache.limit", 20971520); // 20 MB
 
 /* offline cache prefs */
 pref("browser.offline-apps.notify", false);
 pref("browser.cache.offline.enable", true);
 pref("offline-apps.allow_by_default", true);
 
 /* protocol warning prefs */
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -265,16 +265,19 @@ private:
 
   DECL_GFX_PREF(Live, "gl.msaa-level",                         MSAALevel, uint32_t, 2);
   DECL_GFX_PREF(Live, "gl.require-hardware",                   RequireHardwareGL, bool, false);
 
   DECL_GFX_PREF(Once, "image.cache.size",                      ImageCacheSize, int32_t, 5*1024*1024);
   DECL_GFX_PREF(Once, "image.cache.timeweight",                ImageCacheTimeWeight, int32_t, 500);
   DECL_GFX_PREF(Live, "image.decode-immediately.enabled",      ImageDecodeImmediatelyEnabled, bool, false);
   DECL_GFX_PREF(Live, "image.downscale-during-decode.enabled", ImageDownscaleDuringDecodeEnabled, bool, true);
+  DECL_GFX_PREF(Live, "image.high_quality_downscaling.enabled", ImageHQDownscalingEnabled, bool, false);
+  DECL_GFX_PREF(Live, "image.high_quality_downscaling.min_factor", ImageHQDownscalingMinFactor, uint32_t, 1000);
+  DECL_GFX_PREF(Live, "image.high_quality_upscaling.max_size", ImageHQUpscalingMaxSize, uint32_t, 20971520);
   DECL_GFX_PREF(Live, "image.infer-src-animation.threshold-ms", ImageInferSrcAnimationThresholdMS, uint32_t, 2000);
   DECL_GFX_PREF(Once, "image.mem.decode_bytes_at_a_time",      ImageMemDecodeBytesAtATime, uint32_t, 200000);
   DECL_GFX_PREF(Live, "image.mem.discardable",                 ImageMemDiscardable, bool, false);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.discard_factor", ImageMemSurfaceCacheDiscardFactor, uint32_t, 1);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.max_size_kb",    ImageMemSurfaceCacheMaxSizeKB, uint32_t, 100 * 1024);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.min_expiration_ms", ImageMemSurfaceCacheMinExpirationMS, uint32_t, 60*1000);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.size_factor",    ImageMemSurfaceCacheSizeFactor, uint32_t, 64);
   DECL_GFX_PREF(Live, "image.mozsamplesize.enabled",           ImageMozSampleSizeEnabled, bool, false);
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -240,30 +240,16 @@ Decoder::CompleteDecode()
         !(mDecoderFlags & DecoderFlags::IMAGE_IS_TRANSIENT) &&
         mCurrentFrame) {
       mCurrentFrame->SetOptimizable();
     }
   }
 }
 
 nsresult
-Decoder::SetTargetSize(const nsIntSize& aSize)
-{
-  // Make sure the size is reasonable.
-  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
-    return NS_ERROR_FAILURE;
-  }
-
-  // Create a downscaler that we'll filter our output through.
-  mDownscaler.emplace(aSize);
-
-  return NS_OK;
-}
-
-nsresult
 Decoder::AllocateFrame(uint32_t aFrameNum,
                        const nsIntSize& aTargetSize,
                        const nsIntRect& aFrameRect,
                        gfx::SurfaceFormat aFormat,
                        uint8_t aPaletteDepth)
 {
   mCurrentFrame = AllocateFrameInternal(aFrameNum, aTargetSize, aFrameRect,
                                         aFormat, aPaletteDepth,
@@ -333,17 +319,18 @@ Decoder::AllocateFrameInternal(uint32_t 
     return RawAccessFrameRef();
   }
 
   if (ShouldUseSurfaceCache()) {
     InsertOutcome outcome =
       SurfaceCache::Insert(frame, ImageKey(mImage.get()),
                            RasterSurfaceKey(aTargetSize,
                                             mSurfaceFlags,
-                                            aFrameNum));
+                                            aFrameNum),
+                           Lifetime::Persistent);
     if (outcome == InsertOutcome::FAILURE) {
       // We couldn't insert the surface, almost certainly due to low memory. We
       // treat this as a permanent error to help the system recover; otherwise,
       // we might just end up attempting to decode this image again immediately.
       ref->Abort();
       return RawAccessFrameRef();
     } else if (outcome == InsertOutcome::FAILURE_ALREADY_PRESENT) {
       // Another decoder beat us to decoding this frame. We abort this decoder
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -6,17 +6,16 @@
 #ifndef mozilla_image_Decoder_h
 #define mozilla_image_Decoder_h
 
 #include "FrameAnimator.h"
 #include "RasterImage.h"
 #include "mozilla/RefPtr.h"
 #include "DecodePool.h"
 #include "DecoderFlags.h"
-#include "Downscaler.h"
 #include "ImageMetadata.h"
 #include "Orientation.h"
 #include "SourceBuffer.h"
 #include "SurfaceFlags.h"
 
 namespace mozilla {
 
 namespace Telemetry {
@@ -108,24 +107,29 @@ public:
     mMetadataDecode = aMetadataDecode;
   }
   bool IsMetadataDecode() const { return mMetadataDecode; }
 
   /**
    * If this decoder supports downscale-during-decode, sets the target size that
    * this image should be decoded to.
    *
-   * If the provided size is unacceptable, an error is returned.
+   * If this decoder *doesn't* support downscale-during-decode, returns
+   * NS_ERROR_NOT_AVAILABLE. If the provided size is unacceptable, returns
+   * another error.
    *
    * Returning NS_OK from this method is a promise that the decoder will decode
    * the image to the requested target size unless it encounters an error.
    *
    * This must be called before Init() is called.
    */
-  nsresult SetTargetSize(const nsIntSize& aSize);
+  virtual nsresult SetTargetSize(const nsIntSize& aSize)
+  {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
 
   /**
    * Set the requested sample size for this decoder. Used to implement the
    * -moz-sample-size media fragment.
    *
    *  XXX(seth): Support for -moz-sample-size will be removed in bug 1120056.
    */
   virtual void SetSampleSize(int aSampleSize) { }
@@ -394,18 +398,16 @@ protected:
   RawAccessFrameRef AllocateFrameInternal(uint32_t aFrameNum,
                                           const nsIntSize& aTargetSize,
                                           const nsIntRect& aFrameRect,
                                           gfx::SurfaceFormat aFormat,
                                           uint8_t aPaletteDepth,
                                           imgFrame* aPreviousFrame);
 
 protected:
-  Maybe<Downscaler> mDownscaler;
-
   uint8_t* mImageData;  // Pointer to image data in either Cairo or 8bit format
   uint32_t mImageDataLength;
   uint32_t* mColormap;  // Current colormap to be used in Cairo format
   uint32_t mColormapSize;
 
 private:
   nsRefPtr<RasterImage> mImage;
   Maybe<SourceBufferIterator> mIterator;
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -128,16 +128,18 @@ DecoderFactory::CreateDecoder(DecoderTyp
   decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::FIRST_FRAME_ONLY);
   decoder->SetSurfaceFlags(aSurfaceFlags);
   decoder->SetSampleSize(aSampleSize);
   decoder->SetResolution(aResolution);
 
   // Set a target size for downscale-during-decode if applicable.
   if (aTargetSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize);
+    MOZ_ASSERT(nsresult(rv) != NS_ERROR_NOT_AVAILABLE,
+               "We're downscale-during-decode but decoder doesn't support it?");
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
   }
 
   decoder->Init();
   if (NS_FAILED(decoder->GetDecoderError())) {
     return nullptr;
   }
 
--- a/image/Image.h
+++ b/image/Image.h
@@ -142,24 +142,29 @@ public:
    * possible, regardless of what our heuristics say.
    *
    * INIT_FLAG_TRANSIENT: The container is likely to exist for only a short time
    * before being destroyed. (For example, containers for
    * multipart/x-mixed-replace image parts fall into this category.) If this
    * flag is set, INIT_FLAG_DISCARDABLE and INIT_FLAG_DECODE_ONLY_ON_DRAW must
    * not be set.
    *
+   * INIT_FLAG_DOWNSCALE_DURING_DECODE: The container should attempt to
+   * downscale images during decoding instead of decoding them to their
+   * intrinsic size.
+   *
    * INIT_FLAG_SYNC_LOAD: The container is being loaded synchronously, so
    * it should avoid relying on async workers to get the container ready.
    */
   static const uint32_t INIT_FLAG_NONE                     = 0x0;
   static const uint32_t INIT_FLAG_DISCARDABLE              = 0x1;
   static const uint32_t INIT_FLAG_DECODE_IMMEDIATELY       = 0x2;
   static const uint32_t INIT_FLAG_TRANSIENT                = 0x4;
-  static const uint32_t INIT_FLAG_SYNC_LOAD                = 0x8;
+  static const uint32_t INIT_FLAG_DOWNSCALE_DURING_DECODE  = 0x8;
+  static const uint32_t INIT_FLAG_SYNC_LOAD                = 0x10;
 
   virtual already_AddRefed<ProgressTracker> GetProgressTracker() = 0;
   virtual void SetProgressTracker(ProgressTracker* aProgressTracker) {}
 
   /**
    * The size, in bytes, occupied by the compressed source data of the image.
    * If MallocSizeOf does not work on this platform, uses a fallback approach to
    * ensure that something reasonable is always returned.
@@ -207,17 +212,18 @@ public:
    * @param aLastPart Whether this is the final part of the underlying request.
    */
   virtual nsresult OnImageDataComplete(nsIRequest* aRequest,
                                        nsISupports* aContext,
                                        nsresult aStatus,
                                        bool aLastPart) = 0;
 
   /**
-   * Called when the SurfaceCache discards a surface belonging to this image.
+   * Called when the SurfaceCache discards a persistent surface belonging to
+   * this image.
    */
   virtual void OnSurfaceDiscarded() = 0;
 
   virtual void SetInnerWindowID(uint64_t aInnerWindowId) = 0;
   virtual uint64_t InnerWindowID() const = 0;
 
   virtual bool HasError() = 0;
   virtual void SetHasError() = 0;
--- a/image/ImageFactory.cpp
+++ b/image/ImageFactory.cpp
@@ -27,57 +27,78 @@
 
 namespace mozilla {
 namespace image {
 
 /*static*/ void
 ImageFactory::Initialize()
 { }
 
+static bool
+ShouldDownscaleDuringDecode(const nsCString& aMimeType)
+{
+  DecoderType type = DecoderFactory::GetDecoderType(aMimeType.get());
+  return type == DecoderType::JPEG ||
+         type == DecoderType::ICON ||
+         type == DecoderType::ICO ||
+         type == DecoderType::PNG ||
+         type == DecoderType::BMP ||
+         type == DecoderType::GIF;
+}
+
 static uint32_t
 ComputeImageFlags(ImageURL* uri, const nsCString& aMimeType, bool isMultiPart)
 {
   nsresult rv;
 
   // We default to the static globals.
   bool isDiscardable = gfxPrefs::ImageMemDiscardable();
   bool doDecodeImmediately = gfxPrefs::ImageDecodeImmediatelyEnabled();
+  bool doDownscaleDuringDecode = gfxPrefs::ImageDownscaleDuringDecodeEnabled();
 
   // We want UI to be as snappy as possible and not to flicker. Disable
   // discarding for chrome URLS.
   bool isChrome = false;
   rv = uri->SchemeIs("chrome", &isChrome);
   if (NS_SUCCEEDED(rv) && isChrome) {
     isDiscardable = false;
   }
 
   // We don't want resources like the "loading" icon to be discardable either.
   bool isResource = false;
   rv = uri->SchemeIs("resource", &isResource);
   if (NS_SUCCEEDED(rv) && isResource) {
     isDiscardable = false;
   }
 
+  // Downscale-during-decode is only enabled for certain content types.
+  if (doDownscaleDuringDecode && !ShouldDownscaleDuringDecode(aMimeType)) {
+    doDownscaleDuringDecode = false;
+  }
+
   // For multipart/x-mixed-replace, we basically want a direct channel to the
   // decoder. Disable everything for this case.
   if (isMultiPart) {
-    isDiscardable = false;
+    isDiscardable = doDownscaleDuringDecode = false;
   }
 
   // We have all the information we need.
   uint32_t imageFlags = Image::INIT_FLAG_NONE;
   if (isDiscardable) {
     imageFlags |= Image::INIT_FLAG_DISCARDABLE;
   }
   if (doDecodeImmediately) {
     imageFlags |= Image::INIT_FLAG_DECODE_IMMEDIATELY;
   }
   if (isMultiPart) {
     imageFlags |= Image::INIT_FLAG_TRANSIENT;
   }
+  if (doDownscaleDuringDecode) {
+    imageFlags |= Image::INIT_FLAG_DOWNSCALE_DURING_DECODE;
+  }
 
   return imageFlags;
 }
 
 /* static */ already_AddRefed<Image>
 ImageFactory::CreateImage(nsIRequest* aRequest,
                           ProgressTracker* aProgressTracker,
                           const nsCString& aMimeType,
@@ -117,17 +138,23 @@ ImageFactory::CreateAnonymousImage(const
   nsresult rv;
 
   nsRefPtr<RasterImage> newImage = new RasterImage();
 
   nsRefPtr<ProgressTracker> newTracker = new ProgressTracker();
   newTracker->SetImage(newImage);
   newImage->SetProgressTracker(newTracker);
 
-  rv = newImage->Init(aMimeType.get(), Image::INIT_FLAG_SYNC_LOAD);
+  uint32_t imageFlags = Image::INIT_FLAG_SYNC_LOAD;
+  if (gfxPrefs::ImageDownscaleDuringDecodeEnabled() &&
+      ShouldDownscaleDuringDecode(aMimeType)) {
+    imageFlags |= Image::INIT_FLAG_DOWNSCALE_DURING_DECODE;
+  }
+
+  rv = newImage->Init(aMimeType.get(), imageFlags);
   if (NS_FAILED(rv)) {
     return BadImage("RasterImage::Init failed", newImage);
   }
 
   return newImage.forget();
 }
 
 /* static */ already_AddRefed<MultipartImage>
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -60,16 +60,152 @@ namespace image {
 
 using std::ceil;
 using std::min;
 
 // The maximum number of times any one RasterImage was decoded.  This is only
 // used for statistics.
 static int32_t sMaxDecodeCount = 0;
 
+class ScaleRunner : public nsRunnable
+{
+  enum ScaleState
+  {
+    eNew,
+    eReady,
+    eFinish,
+    eFinishWithError
+  };
+
+public:
+  ScaleRunner(RasterImage* aImage,
+              uint32_t aImageFlags,
+              const IntSize& aSize,
+              RawAccessFrameRef&& aSrcRef)
+    : mImage(aImage)
+    , mSrcRef(Move(aSrcRef))
+    , mDstSize(aSize)
+    , mImageFlags(aImageFlags)
+    , mState(eNew)
+  {
+    MOZ_ASSERT(!mSrcRef->GetIsPaletted());
+    MOZ_ASSERT(aSize.width > 0 && aSize.height > 0);
+  }
+
+  bool Init()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mState == eNew, "Calling Init() twice?");
+
+    // We'll need a destination frame. It's unconditionally ARGB32 because
+    // that's what the scaler outputs.
+    nsRefPtr<imgFrame> tentativeDstFrame = new imgFrame();
+    nsresult rv =
+      tentativeDstFrame->InitForDecoder(mDstSize, SurfaceFormat::B8G8R8A8);
+    if (NS_FAILED(rv)) {
+      return false;
+    }
+
+    // We need a strong reference to the raw data for the destination frame.
+    // (We already got one for the source frame in the constructor.)
+    RawAccessFrameRef tentativeDstRef = tentativeDstFrame->RawAccessRef();
+    if (!tentativeDstRef) {
+      return false;
+    }
+
+    // Everything worked, so commit to these objects and mark ourselves ready.
+    mDstRef = Move(tentativeDstRef);
+    mState = eReady;
+
+    // Insert the new surface into the cache immediately. We need to do this so
+    // that we won't start multiple scaling jobs for the same size.
+    SurfaceCache::Insert(mDstRef.get(), ImageKey(mImage.get()),
+                         RasterSurfaceKey(mDstSize,
+                                          ToSurfaceFlags(mImageFlags),
+                                          /* aFrameNum = */ 0),
+                         Lifetime::Transient);
+
+    return true;
+  }
+
+  NS_IMETHOD Run() override
+  {
+    if (mState == eReady) {
+      // Collect information from the frames that we need to scale.
+      ScalingData srcData = mSrcRef->GetScalingData();
+      ScalingData dstData = mDstRef->GetScalingData();
+
+      // Actually do the scaling.
+      bool succeeded =
+        gfx::Scale(srcData.mRawData, srcData.mSize.width, srcData.mSize.height,
+                   srcData.mBytesPerRow, dstData.mRawData, mDstSize.width,
+                   mDstSize.height, dstData.mBytesPerRow, srcData.mFormat);
+
+      if (succeeded) {
+        // Mark the frame as complete and discardable.
+        mDstRef->ImageUpdated(mDstRef->GetRect());
+        MOZ_ASSERT(mDstRef->IsImageComplete(),
+                   "Incomplete, but just updated the entire frame");
+      }
+
+      // We need to send notifications and release our references on the main
+      // thread, so finish up there.
+      mState = succeeded ? eFinish : eFinishWithError;
+      NS_DispatchToMainThread(this);
+    } else if (mState == eFinish) {
+      MOZ_ASSERT(NS_IsMainThread());
+      MOZ_ASSERT(mDstRef, "Should have a valid scaled frame");
+
+      // Notify, so observers can redraw.
+      nsRefPtr<RasterImage> image = mImage.get();
+      if (image) {
+        image->NotifyNewScaledFrame();
+      }
+
+      // We're done, so release everything.
+      mSrcRef.reset();
+      mDstRef.reset();
+    } else if (mState == eFinishWithError) {
+      MOZ_ASSERT(NS_IsMainThread());
+      NS_WARNING("HQ scaling failed");
+
+      // Remove the frame from the cache since we know we don't need it.
+      SurfaceCache::RemoveSurface(ImageKey(mImage.get()),
+                                  RasterSurfaceKey(mDstSize,
+                                                   ToSurfaceFlags(mImageFlags),
+                                                   /* aFrameNum = */ 0));
+
+      // Release everything we're holding, too.
+      mSrcRef.reset();
+      mDstRef.reset();
+    } else {
+      // mState must be eNew, which is invalid in Run().
+      MOZ_ASSERT(false, "Need to call Init() before dispatching");
+    }
+
+    return NS_OK;
+  }
+
+private:
+  virtual ~ScaleRunner()
+  {
+    MOZ_ASSERT(!mSrcRef && !mDstRef,
+               "Should have released strong refs in Run()");
+  }
+
+  WeakPtr<RasterImage> mImage;
+  RawAccessFrameRef    mSrcRef;
+  RawAccessFrameRef    mDstRef;
+  const IntSize      mDstSize;
+  uint32_t             mImageFlags;
+  ScaleState           mState;
+};
+
+static nsCOMPtr<nsIThread> sScaleWorkerThread = nullptr;
+
 #ifndef DEBUG
 NS_IMPL_ISUPPORTS(RasterImage, imgIContainer, nsIProperties)
 #else
 NS_IMPL_ISUPPORTS(RasterImage, imgIContainer, nsIProperties,
                   imgIContainerDebug)
 #endif
 
 //******************************************************************************
@@ -86,16 +222,17 @@ RasterImage::RasterImage(ImageURL* aURI 
   mSourceBuffer(new SourceBuffer()),
   mFrameCount(0),
   mHasSize(false),
   mTransient(false),
   mSyncLoad(false),
   mDiscardable(false),
   mHasSourceData(false),
   mHasBeenDecoded(false),
+  mDownscaleDuringDecode(false),
   mPendingAnimation(false),
   mAnimationFinished(false),
   mWantFullDecode(false)
 {
   Telemetry::GetHistogramById(Telemetry::IMAGE_DECODE_COUNT)->Add(0);
 }
 
 //******************************************************************************
@@ -121,25 +258,33 @@ RasterImage::Init(const char* aMimeType,
   }
 
   // Not sure an error can happen before init, but be safe
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   // We want to avoid redecodes for transient images.
-  MOZ_ASSERT_IF(aFlags & INIT_FLAG_TRANSIENT,
-                !(aFlags & INIT_FLAG_DISCARDABLE));
+  MOZ_ASSERT(!(aFlags & INIT_FLAG_TRANSIENT) ||
+               (!(aFlags & INIT_FLAG_DISCARDABLE) &&
+                !(aFlags & INIT_FLAG_DOWNSCALE_DURING_DECODE)),
+             "Illegal init flags for transient image");
 
   // Store initialization data
   mDiscardable = !!(aFlags & INIT_FLAG_DISCARDABLE);
   mWantFullDecode = !!(aFlags & INIT_FLAG_DECODE_IMMEDIATELY);
   mTransient = !!(aFlags & INIT_FLAG_TRANSIENT);
+  mDownscaleDuringDecode = !!(aFlags & INIT_FLAG_DOWNSCALE_DURING_DECODE);
   mSyncLoad = !!(aFlags & INIT_FLAG_SYNC_LOAD);
 
+#ifndef MOZ_ENABLE_SKIA
+  // Downscale-during-decode requires Skia.
+  mDownscaleDuringDecode = false;
+#endif
+
   // Use the MIME type to select a decoder type, and make sure there *is* a
   // decoder for this MIME type.
   NS_ENSURE_ARG_POINTER(aMimeType);
   mDecoderType = DecoderFactory::GetDecoderType(aMimeType);
   if (mDecoderType == DecoderType::UNKNOWN) {
     return NS_ERROR_FAILURE;
   }
 
@@ -1193,29 +1338,33 @@ RasterImage::RequestDecodeForSize(const 
     return NS_ERROR_FAILURE;
   }
 
   if (!mHasSize) {
     mWantFullDecode = true;
     return NS_OK;
   }
 
+  // Fall back to our intrinsic size if we don't support
+  // downscale-during-decode.
+  IntSize targetSize = mDownscaleDuringDecode ? aSize : mSize;
+
   // Decide whether to sync decode images we can decode quickly. Here we are
   // explicitly trading off flashing for responsiveness in the case that we're
   // redecoding an image (see bug 845147).
   bool shouldSyncDecodeIfFast =
     !mHasBeenDecoded && (aFlags & FLAG_SYNC_DECODE_IF_FAST);
 
   uint32_t flags = shouldSyncDecodeIfFast
                  ? aFlags
                  : aFlags & ~FLAG_SYNC_DECODE_IF_FAST;
 
   // Look up the first frame of the image, which will implicitly start decoding
   // if it's not available right now.
-  LookupFrame(0, aSize, flags);
+  LookupFrame(0, targetSize, flags);
 
   return NS_OK;
 }
 
 static void
 LaunchDecoder(Decoder* aDecoder,
               RasterImage* aImage,
               uint32_t aFlags,
@@ -1255,24 +1404,30 @@ RasterImage::Decode(const IntSize& aSize
   }
 
   // If we don't have a size yet, we can't do any other decoding.
   if (!mHasSize) {
     mWantFullDecode = true;
     return NS_OK;
   }
 
-  // We're about to decode again, which may mean that some of the previous sizes
-  // we've decoded at aren't useful anymore. We can allow them to expire from
-  // the cache by unlocking them here. When the decode finishes, it will send an
-  // invalidation that will cause all instances of this image to redraw. If this
-  // image is locked, any surfaces that are still useful will become locked
-  // again when LookupFrame touches them, and the remainder will eventually
-  // expire.
-  SurfaceCache::UnlockSurfaces(ImageKey(this));
+  if (mDownscaleDuringDecode) {
+    // We're about to decode again, which may mean that some of the previous
+    // sizes we've decoded at aren't useful anymore. We can allow them to
+    // expire from the cache by unlocking them here. When the decode finishes,
+    // it will send an invalidation that will cause all instances of this image
+    // to redraw. If this image is locked, any surfaces that are still useful
+    // will become locked again when LookupFrame touches them, and the remainder
+    // will eventually expire.
+    SurfaceCache::UnlockSurfaces(ImageKey(this));
+  }
+
+  MOZ_ASSERT(mDownscaleDuringDecode || aSize == mSize,
+             "Can only decode to our intrinsic size if we're not allowed to "
+             "downscale-during-decode");
 
   Maybe<IntSize> targetSize = mSize != aSize ? Some(aSize) : Nothing();
 
   // Determine which flags we need to decode this image with.
   DecoderFlags decoderFlags = DefaultDecoderFlags();
   if (aFlags & FLAG_ASYNC_NOTIFY) {
     decoderFlags |= DecoderFlags::ASYNC_NOTIFY;
   }
@@ -1387,34 +1542,85 @@ RasterImage::RecoverFromInvalidFrames(co
     ResetAnimation();
     return;
   }
 
   // For non-animated images, it's fine to recover using an async decode.
   Decode(aSize, aFlags);
 }
 
-static bool
-HaveSkia()
+bool
+RasterImage::CanScale(GraphicsFilter aFilter,
+                      const IntSize& aSize,
+                      uint32_t aFlags)
 {
-#ifdef MOZ_ENABLE_SKIA
-  return true;
+#ifndef MOZ_ENABLE_SKIA
+  // The high-quality scaler requires Skia.
+  return false;
 #else
-  return false;
+  // Check basic requirements: HQ downscaling is enabled, we have all the source
+  // data and know our size, the flags allow us to do it, and a 'good' filter is
+  // being used. The flags may ask us not to scale because the caller isn't
+  // drawing to the window. If we're drawing to something else (e.g. a canvas)
+  // we usually have no way of updating what we've drawn, so HQ scaling is
+  // useless.
+  if (!gfxPrefs::ImageHQDownscalingEnabled() || !mHasSize || !mHasSourceData ||
+      !(aFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING) ||
+      aFilter != GraphicsFilter::FILTER_GOOD) {
+    return false;
+  }
+
+  // We don't HQ scale images that we can downscale during decode.
+  if (mDownscaleDuringDecode) {
+    return false;
+  }
+
+  // We don't use the scaler for animated or transient images to avoid doing a
+  // bunch of work on an image that just gets thrown away.
+  if (mAnim || mTransient) {
+    return false;
+  }
+
+  // If target size is 1:1 with original, don't scale.
+  if (aSize == mSize) {
+    return false;
+  }
+
+  // To save memory, don't quality upscale images bigger than the limit.
+  if (aSize.width > mSize.width || aSize.height > mSize.height) {
+    uint32_t scaledSize = static_cast<uint32_t>(aSize.width * aSize.height);
+    if (scaledSize > gfxPrefs::ImageHQUpscalingMaxSize()) {
+      return false;
+    }
+  }
+
+  // There's no point in scaling if we can't store the result.
+  if (!SurfaceCache::CanHold(aSize)) {
+    return false;
+  }
+
+  // XXX(seth): It's not clear what this check buys us over
+  // gfxPrefs::ImageHQUpscalingMaxSize().
+  // The default value of this pref is 1000, which means that we never upscale.
+  // If that's all it's getting us, I'd rather we just forbid that explicitly.
+  gfx::Size scale(double(aSize.width) / mSize.width,
+                  double(aSize.height) / mSize.height);
+  gfxFloat minFactor = gfxPrefs::ImageHQDownscalingMinFactor() / 1000.0;
+  return (scale.width < minFactor || scale.height < minFactor);
 #endif
 }
 
 bool
 RasterImage::CanDownscaleDuringDecode(const IntSize& aSize, uint32_t aFlags)
 {
-  // Check basic requirements: downscale-during-decode is enabled, Skia is
-  // available, this image isn't transient, we have all the source data and know
-  // our size, and the flags allow us to do it.
-  if (!mHasSize || mTransient || !HaveSkia() ||
-      !gfxPrefs::ImageDownscaleDuringDecodeEnabled() ||
+  // Check basic requirements: downscale-during-decode is enabled for this
+  // image, we have all the source data and know our size, the flags allow us to
+  // do it, and a 'good' filter is being used.
+  if (!mDownscaleDuringDecode || !mHasSize ||
+      !gfxPrefs::ImageHQDownscalingEnabled() ||
       !(aFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING)) {
     return false;
   }
 
   // We don't downscale animated images during decode.
   if (mAnim) {
     return false;
   }
@@ -1432,42 +1638,109 @@ RasterImage::CanDownscaleDuringDecode(co
   // There's no point in scaling if we can't store the result.
   if (!SurfaceCache::CanHold(aSize)) {
     return false;
   }
 
   return true;
 }
 
+void
+RasterImage::NotifyNewScaledFrame()
+{
+  // Send an invalidation so observers will repaint and can take advantage of
+  // the new scaled frame if possible.
+  NotifyProgress(NoProgress, IntRect(0, 0, mSize.width, mSize.height));
+}
+
+void
+RasterImage::RequestScale(imgFrame* aFrame,
+                          uint32_t aFlags,
+                          const IntSize& aSize)
+{
+  // We don't scale frames which aren't fully decoded.
+  if (!aFrame->IsImageComplete()) {
+    return;
+  }
+
+  // We can't scale frames that need padding or are single pixel.
+  if (aFrame->NeedsPadding() || aFrame->IsSinglePixel()) {
+    return;
+  }
+
+  // We also can't scale if we can't lock the image data for this frame.
+  RawAccessFrameRef frameRef = aFrame->RawAccessRef();
+  if (!frameRef) {
+    return;
+  }
+
+  nsRefPtr<ScaleRunner> runner =
+    new ScaleRunner(this, aFlags, aSize, Move(frameRef));
+  if (runner->Init()) {
+    if (!sScaleWorkerThread) {
+      NS_NewNamedThread("Image Scaler", getter_AddRefs(sScaleWorkerThread));
+      ClearOnShutdown(&sScaleWorkerThread);
+    }
+
+    sScaleWorkerThread->Dispatch(runner, NS_DISPATCH_NORMAL);
+  }
+}
+
 DrawResult
-RasterImage::DrawInternal(DrawableFrameRef&& aFrameRef,
-                          gfxContext* aContext,
-                          const IntSize& aSize,
-                          const ImageRegion& aRegion,
-                          GraphicsFilter aFilter,
-                          uint32_t aFlags)
+RasterImage::DrawWithPreDownscaleIfNeeded(DrawableFrameRef&& aFrameRef,
+                                          gfxContext* aContext,
+                                          const IntSize& aSize,
+                                          const ImageRegion& aRegion,
+                                          GraphicsFilter aFilter,
+                                          uint32_t aFlags)
 {
+  DrawableFrameRef frameRef;
+
+  if (CanScale(aFilter, aSize, aFlags)) {
+    LookupResult result =
+      SurfaceCache::Lookup(ImageKey(this),
+                           RasterSurfaceKey(aSize,
+                                            ToSurfaceFlags(aFlags),
+                                            /* aFrameNum = */ 0));
+    if (!result) {
+      // We either didn't have a matching scaled frame or the OS threw it away.
+      // Request a new one so we'll be ready next time. For now, we'll fall back
+      // to aFrameRef below.
+      RequestScale(aFrameRef.get(), aFlags, aSize);
+    }
+    if (result && result.DrawableRef()->IsImageComplete()) {
+      frameRef = Move(result.DrawableRef());  // The scaled version is ready.
+    }
+  }
+
   gfxContextMatrixAutoSaveRestore saveMatrix(aContext);
   ImageRegion region(aRegion);
-  bool frameIsComplete = aFrameRef->IsImageComplete();
+  bool frameIsComplete = true;  // We already checked HQ scaled frames.
+  if (!frameRef) {
+    // There's no HQ scaled frame available, so we'll have to use the frame
+    // provided by the caller.
+    frameRef = Move(aFrameRef);
+    frameIsComplete = frameRef->IsImageComplete();
+  }
 
   // By now we may have a frame with the requested size. If not, we need to
   // adjust the drawing parameters accordingly.
-  IntSize finalSize = aFrameRef->GetImageSize();
+  IntSize finalSize = frameRef->GetImageSize();
   bool couldRedecodeForBetterFrame = false;
   if (finalSize != aSize) {
     gfx::Size scale(double(aSize.width) / finalSize.width,
                     double(aSize.height) / finalSize.height);
     aContext->Multiply(gfxMatrix::Scaling(scale.width, scale.height));
     region.Scale(1.0 / scale.width, 1.0 / scale.height);
 
-    couldRedecodeForBetterFrame = CanDownscaleDuringDecode(aSize, aFlags);
+    couldRedecodeForBetterFrame = mDownscaleDuringDecode &&
+                                  CanDownscaleDuringDecode(aSize, aFlags);
   }
 
-  if (!aFrameRef->Draw(aContext, region, aFilter, aFlags)) {
+  if (!frameRef->Draw(aContext, region, aFilter, aFlags)) {
     RecoverFromInvalidFrames(aSize, aFlags);
     return DrawResult::TEMPORARY_ERROR;
   }
   if (!frameIsComplete) {
     return DrawResult::INCOMPLETE;
   }
   if (couldRedecodeForBetterFrame) {
     return DrawResult::WRONG_SIZE;
@@ -1531,18 +1804,18 @@ RasterImage::Draw(gfxContext* aContext,
       mDrawStartTime = TimeStamp::Now();
     }
     return DrawResult::NOT_READY;
   }
 
   bool shouldRecordTelemetry = !mDrawStartTime.IsNull() &&
                                ref->IsImageComplete();
 
-  auto result = DrawInternal(Move(ref), aContext, aSize,
-                             aRegion, aFilter, flags);
+  auto result = DrawWithPreDownscaleIfNeeded(Move(ref), aContext, aSize,
+                                             aRegion, aFilter, flags);
 
   if (shouldRecordTelemetry) {
       TimeDuration drawLatency = TimeStamp::Now() - mDrawStartTime;
       Telemetry::Accumulate(Telemetry::IMAGE_DECODE_ON_DRAW_LATENCY,
                             int32_t(drawLatency.ToMicroseconds()));
       mDrawStartTime = TimeStamp();
   }
 
@@ -1846,16 +2119,35 @@ RasterImage::OptimalImageSizeForDest(con
     return IntSize(0, 0);
   }
 
   IntSize destSize(ceil(aDest.width), ceil(aDest.height));
 
   if (aFilter == GraphicsFilter::FILTER_GOOD &&
       CanDownscaleDuringDecode(destSize, aFlags)) {
     return destSize;
+  } else if (CanScale(aFilter, destSize, aFlags)) {
+    LookupResult result =
+      SurfaceCache::Lookup(ImageKey(this),
+                           RasterSurfaceKey(destSize,
+                                            ToSurfaceFlags(aFlags),
+                                            /* aFrameNum = */ 0));
+
+    if (result && result.DrawableRef()->IsImageComplete()) {
+      return destSize;  // We have an existing HQ scale for this size.
+    }
+    if (!result) {
+      // We could HQ scale to this size, but we haven't. Request a scale now.
+      DrawableFrameRef ref = LookupFrame(GetRequestedFrameIndex(aWhichFrame),
+                                         mSize, aFlags);
+      if (ref) {
+        RequestScale(ref.get(), aFlags, destSize);
+      }
+    }
   }
 
-  // We can't scale to this size. Use our intrinsic size for now.
+  // We either can't HQ scale to this size or the scaled version isn't ready
+  // yet. Use our intrinsic size for now.
   return mSize;
 }
 
 } // namespace image
 } // namespace mozilla
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -249,22 +249,22 @@ public:
       GetURI()->GetSpec(spec);
     }
     return spec;
   }
 
 private:
   nsresult Init(const char* aMimeType, uint32_t aFlags);
 
-  DrawResult DrawInternal(DrawableFrameRef&& aFrameRef,
-                          gfxContext* aContext,
-                          const nsIntSize& aSize,
-                          const ImageRegion& aRegion,
-                          GraphicsFilter aFilter,
-                          uint32_t aFlags);
+  DrawResult DrawWithPreDownscaleIfNeeded(DrawableFrameRef&& aFrameRef,
+                                          gfxContext* aContext,
+                                          const nsIntSize& aSize,
+                                          const ImageRegion& aRegion,
+                                          GraphicsFilter aFilter,
+                                          uint32_t aFlags);
 
   already_AddRefed<gfx::SourceSurface> CopyFrame(uint32_t aWhichFrame,
                                              uint32_t aFlags);
 
   Pair<DrawResult, RefPtr<gfx::SourceSurface>>
     GetFrameInternal(const gfx::IntSize& aSize,
                      uint32_t aWhichFrame,
                      uint32_t aFlags);
@@ -300,16 +300,20 @@ private:
 
   /**
    * Creates and runs a decoder, either synchronously or asynchronously
    * according to @aFlags. Decodes at the provided target size @aSize, using
    * decode flags @aFlags.
    *
    * It's an error to call Decode() before this image's intrinsic size is
    * available. A metadata decode must successfully complete first.
+   *
+   * If downscale-during-decode is not enabled for this image (i.e., if
+   * mDownscaleDuringDecode is false), it is an error to pass an @aSize value
+   * different from this image's intrinsic size.
    */
   NS_IMETHOD Decode(const gfx::IntSize& aSize, uint32_t aFlags);
 
   /**
    * Creates and runs a metadata decoder, either synchronously or
    * asynchronously according to @aFlags.
    */
   NS_IMETHOD DecodeMetadata(uint32_t aFlags);
@@ -389,16 +393,17 @@ private: // data
 
   // Boolean flags (clustered together to conserve space):
   bool                       mHasSize:1;       // Has SetSize() been called?
   bool                       mTransient:1;     // Is the image short-lived?
   bool                       mSyncLoad:1;      // Are we loading synchronously?
   bool                       mDiscardable:1;   // Is container discardable?
   bool                       mHasSourceData:1; // Do we have source data?
   bool                       mHasBeenDecoded:1; // Decoded at least once?
+  bool                       mDownscaleDuringDecode:1;
 
   // Whether we're waiting to start animation. If we get a StartAnimation() call
   // but we don't yet have more than one frame, mPendingAnimation is set so that
   // we know to start animation later if/when we have more frames.
   bool                       mPendingAnimation:1;
 
   // Whether the animation can stop, due to running out
   // of frames, or no more owning request
@@ -410,20 +415,32 @@ private: // data
 
   TimeStamp mDrawStartTime;
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Scaling.
   //////////////////////////////////////////////////////////////////////////////
 
+  // Initiates an HQ scale for the given frame, if possible.
+  void RequestScale(imgFrame* aFrame, uint32_t aFlags, const nsIntSize& aSize);
+
+  // Determines whether we can perform an HQ scale with the given parameters.
+  bool CanScale(GraphicsFilter aFilter, const nsIntSize& aSize,
+                uint32_t aFlags);
+
   // Determines whether we can downscale during decode with the given
   // parameters.
   bool CanDownscaleDuringDecode(const nsIntSize& aSize, uint32_t aFlags);
 
+  // Called by the HQ scaler when a new scaled frame is ready.
+  void NotifyNewScaledFrame();
+
+  friend class ScaleRunner;
+
 
   // Error handling.
   void DoError();
 
   class HandleErrorWorker : public nsRunnable
   {
   public:
     /**
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -130,24 +130,27 @@ class CachedSurface
   ~CachedSurface() { }
 public:
   MOZ_DECLARE_REFCOUNTED_TYPENAME(CachedSurface)
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CachedSurface)
 
   CachedSurface(imgFrame*          aSurface,
                 const Cost         aCost,
                 const ImageKey     aImageKey,
-                const SurfaceKey&  aSurfaceKey)
+                const SurfaceKey&  aSurfaceKey,
+                const Lifetime     aLifetime)
     : mSurface(aSurface)
     , mCost(aCost)
     , mImageKey(aImageKey)
     , mSurfaceKey(aSurfaceKey)
+    , mLifetime(aLifetime)
   {
-    MOZ_ASSERT(!IsPlaceholder() || mCost == sPlaceholderCost,
-               "Placeholder should have trivial cost");
+    MOZ_ASSERT(!IsPlaceholder() ||
+               (mCost == sPlaceholderCost && mLifetime == Lifetime::Transient),
+               "Placeholder should have trivial cost and transient lifetime");
     MOZ_ASSERT(mImageKey, "Must have a valid image key");
   }
 
   DrawableFrameRef DrawableRef() const
   {
     if (MOZ_UNLIKELY(IsPlaceholder())) {
       MOZ_ASSERT_UNREACHABLE("Shouldn't call DrawableRef() on a placeholder");
       return DrawableFrameRef();
@@ -157,33 +160,34 @@ public:
   }
 
   void SetLocked(bool aLocked)
   {
     if (IsPlaceholder()) {
       return;  // Can't lock a placeholder.
     }
 
-    if (aLocked) {
+    if (aLocked && mLifetime == Lifetime::Persistent) {
       // This may fail, and that's OK. We make no guarantees about whether
       // locking is successful if you call SurfaceCache::LockImage() after
       // SurfaceCache::Insert().
       mDrawableRef = mSurface->DrawableRef();
     } else {
       mDrawableRef.reset();
     }
   }
 
   bool IsPlaceholder() const { return !bool(mSurface); }
   bool IsLocked() const { return bool(mDrawableRef); }
 
   ImageKey GetImageKey() const { return mImageKey; }
   SurfaceKey GetSurfaceKey() const { return mSurfaceKey; }
   CostEntry GetCostEntry() { return image::CostEntry(this, mCost); }
   nsExpirationState* GetExpirationState() { return &mExpirationState; }
+  Lifetime GetLifetime() const { return mLifetime; }
 
   bool IsDecoded() const
   {
     return !IsPlaceholder() && mSurface->IsImageComplete();
   }
 
   // A helper type used by SurfaceCacheImpl::CollectSizeOfSurfaces.
   struct MOZ_STACK_CLASS SurfaceMemoryReport
@@ -221,16 +225,17 @@ public:
 
 private:
   nsExpirationState  mExpirationState;
   nsRefPtr<imgFrame> mSurface;
   DrawableFrameRef   mDrawableRef;
   const Cost         mCost;
   const ImageKey     mImageKey;
   const SurfaceKey   mSurfaceKey;
+  const Lifetime     mLifetime;
 };
 
 /**
  * An ImageSurfaceCache is a per-image surface cache. For correctness we must be
  * able to remove all surfaces associated with an image when the image is
  * destroyed or invalidated. Since this will happen frequently, it makes sense
  * to make it cheap by storing the surfaces for each image separately.
  *
@@ -249,18 +254,19 @@ public:
   typedef
     nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface> SurfaceTable;
 
   bool IsEmpty() const { return mSurfaces.Count() == 0; }
 
   void Insert(const SurfaceKey& aKey, CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
-    MOZ_ASSERT(!mLocked || aSurface->IsPlaceholder() || aSurface->IsLocked(),
-               "Inserting an unlocked surface for a locked image");
+    MOZ_ASSERT(!mLocked || aSurface->GetLifetime() != Lifetime::Persistent ||
+               aSurface->IsLocked(),
+               "Inserting an unlocked persistent surface for a locked image");
     mSurfaces.Put(aKey, aSurface);
   }
 
   void Remove(CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
     MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()),
         "Should not be removing a surface we don't have");
@@ -459,17 +465,18 @@ private:
 public:
   void InitMemoryReporter() { RegisterWeakMemoryReporter(this); }
 
   Mutex& GetMutex() { return mMutex; }
 
   InsertOutcome Insert(imgFrame*         aSurface,
                        const Cost        aCost,
                        const ImageKey    aImageKey,
-                       const SurfaceKey& aSurfaceKey)
+                       const SurfaceKey& aSurfaceKey,
+                       Lifetime          aLifetime)
   {
     // If this is a duplicate surface, refuse to replace the original.
     // XXX(seth): Calling Lookup() and then RemoveSurface() does the lookup
     // twice. We'll make this more efficient in bug 1185137.
     LookupResult result = Lookup(aImageKey, aSurfaceKey, /* aMarkUsed = */ false);
     if (MOZ_UNLIKELY(result)) {
       return InsertOutcome::FAILURE_ALREADY_PRESENT;
     }
@@ -501,22 +508,22 @@ public:
     // for this image, create it.
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache) {
       cache = new ImageSurfaceCache;
       mImageCaches.Put(aImageKey, cache);
     }
 
     nsRefPtr<CachedSurface> surface =
-      new CachedSurface(aSurface, aCost, aImageKey, aSurfaceKey);
+      new CachedSurface(aSurface, aCost, aImageKey, aSurfaceKey, aLifetime);
 
-    // We require that locking succeed if the image is locked and we're not
-    // inserting a placeholder; the caller may need to know this to handle
-    // errors correctly.
-    if (cache->IsLocked() && !surface->IsPlaceholder()) {
+    // We require that locking succeed if the image is locked and the surface is
+    // persistent; the caller may need to know this to handle errors correctly.
+    if (cache->IsLocked() && aLifetime == Lifetime::Persistent) {
+      MOZ_ASSERT(!surface->IsPlaceholder(), "Placeholders should be transient");
       surface->SetLocked(true);
       if (!surface->IsLocked()) {
         return InsertOutcome::FAILURE;
       }
     }
 
     // Insert.
     MOZ_ASSERT(aCost <= mAvailableCost, "Inserting despite too large a cost");
@@ -529,18 +536,18 @@ public:
   void Remove(CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
     ImageKey imageKey = aSurface->GetImageKey();
 
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey);
     MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache");
 
-    // If the surface was not a placeholder, tell its image that we discarded it.
-    if (!aSurface->IsPlaceholder()) {
+    // If the surface was persistent, tell its image that we discarded it.
+    if (aSurface->GetLifetime() == Lifetime::Persistent) {
       static_cast<Image*>(imageKey)->OnSurfaceDiscarded();
     }
 
     StopTracking(aSurface);
     cache->Remove(aSurface);
 
     // Remove the per-image cache if it's unneeded now. (Keep it if the image is
     // locked, since the per-image cache is where we store that state.)
@@ -765,18 +772,19 @@ public:
     // The per-image cache isn't needed anymore, so remove it as well.
     // This implicitly unlocks the image if it was locked.
     mImageCaches.Remove(aImageKey);
   }
 
   void DiscardAll()
   {
     // Remove in order of cost because mCosts is an array and the other data
-    // structures are all hash tables. Note that locked surfaces are not
-    // removed, since they aren't present in mCosts.
+    // structures are all hash tables. Note that locked surfaces (persistent
+    // surfaces belonging to locked images) are not removed, since they aren't
+    // present in mCosts.
     while (!mCosts.IsEmpty()) {
       Remove(mCosts.LastElement().GetSurface());
     }
   }
 
   void DiscardForMemoryPressure()
   {
     // Compute our discardable cost. Since locked surfaces aren't discardable,
@@ -800,17 +808,18 @@ public:
     while (mAvailableCost < targetCost) {
       MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and still not done");
       Remove(mCosts.LastElement().GetSurface());
     }
   }
 
   void LockSurface(CachedSurface* aSurface)
   {
-    if (aSurface->IsPlaceholder() || aSurface->IsLocked()) {
+    if (aSurface->GetLifetime() == Lifetime::Transient ||
+        aSurface->IsLocked()) {
       return;
     }
 
     StopTracking(aSurface);
 
     // Lock the surface. This can fail.
     aSurface->SetLocked(true);
     StartTracking(aSurface);
@@ -823,17 +832,18 @@ public:
     static_cast<SurfaceCacheImpl*>(aCache)->StopTracking(aSurface);
     return PL_DHASH_NEXT;
   }
 
   static PLDHashOperator DoUnlockSurface(const SurfaceKey&,
                                          CachedSurface*    aSurface,
                                          void*             aCache)
   {
-    if (aSurface->IsPlaceholder() || !aSurface->IsLocked()) {
+    if (aSurface->GetLifetime() == Lifetime::Transient ||
+        !aSurface->IsLocked()) {
       return PL_DHASH_NEXT;
     }
 
     auto cache = static_cast<SurfaceCacheImpl*>(aCache);
     cache->StopTracking(aSurface);
 
     aSurface->SetLocked(false);
     cache->StartTracking(aSurface);
@@ -906,19 +916,19 @@ private:
   {
     nsRefPtr<ImageSurfaceCache> imageCache;
     mImageCaches.Get(aImageKey, getter_AddRefs(imageCache));
     return imageCache.forget();
   }
 
   // This is similar to CanHold() except that it takes into account the costs of
   // locked surfaces. It's used internally in Insert(), but it's not exposed
-  // publicly because we permit multithreaded access to the surface cache, which
-  // means that the result would be meaningless: another thread could insert a
-  // surface or lock an image at any time.
+  // publicly because if we start permitting multithreaded access to the surface
+  // cache, which seems likely, then the result would be meaningless: another
+  // thread could insert a persistent surface or lock an image at any time.
   bool CanHoldAfterDiscarding(const Cost aCost) const
   {
     return aCost <= mMaxCost - mLockedCost;
   }
 
   void MarkUsed(CachedSurface* aSurface, ImageSurfaceCache* aCache)
   {
     if (aCache->IsLocked()) {
@@ -1081,42 +1091,44 @@ SurfaceCache::LookupBestMatch(const Imag
 
   MutexAutoLock lock(sInstance->GetMutex());
   return sInstance->LookupBestMatch(aImageKey, aSurfaceKey, aAlternateFlags);
 }
 
 /* static */ InsertOutcome
 SurfaceCache::Insert(imgFrame*         aSurface,
                      const ImageKey    aImageKey,
-                     const SurfaceKey& aSurfaceKey)
+                     const SurfaceKey& aSurfaceKey,
+                     Lifetime          aLifetime)
 {
   if (!sInstance) {
     return InsertOutcome::FAILURE;
   }
 
   // Refuse null surfaces.
   if (!aSurface) {
     MOZ_CRASH("Don't pass null surfaces to SurfaceCache::Insert");
   }
 
   MutexAutoLock lock(sInstance->GetMutex());
   Cost cost = ComputeCost(aSurface->GetSize(), aSurface->GetBytesPerPixel());
-  return sInstance->Insert(aSurface, cost, aImageKey, aSurfaceKey);
+  return sInstance->Insert(aSurface, cost, aImageKey, aSurfaceKey, aLifetime);
 }
 
 /* static */ InsertOutcome
 SurfaceCache::InsertPlaceholder(const ImageKey    aImageKey,
                                 const SurfaceKey& aSurfaceKey)
 {
   if (!sInstance) {
     return InsertOutcome::FAILURE;
   }
 
   MutexAutoLock lock(sInstance->GetMutex());
-  return sInstance->Insert(nullptr, sPlaceholderCost, aImageKey, aSurfaceKey);
+  return sInstance->Insert(nullptr, sPlaceholderCost, aImageKey, aSurfaceKey,
+                           Lifetime::Transient);
 }
 
 /* static */ bool
 SurfaceCache::CanHold(const IntSize& aSize, uint32_t aBytesPerPixel /* = 4 */)
 {
   if (!sInstance) {
     return false;
   }
--- a/image/SurfaceCache.h
+++ b/image/SurfaceCache.h
@@ -116,16 +116,21 @@ VectorSurfaceKey(const gfx::IntSize& aSi
                  float aAnimationTime)
 {
   // We don't care about aFlags for VectorImage because none of the flags we
   // have right now influence VectorImage's rendering. If we add a new flag that
   // *does* affect how a VectorImage renders, we'll have to change this.
   return SurfaceKey(aSize, aSVGContext, aAnimationTime, DefaultSurfaceFlags());
 }
 
+enum class Lifetime : uint8_t {
+  Transient,
+  Persistent
+};
+
 enum class InsertOutcome : uint8_t {
   SUCCESS,                 // Success (but see Insert documentation).
   FAILURE,                 // Couldn't insert (e.g., for capacity reasons).
   FAILURE_ALREADY_PRESENT  // A surface with the same key is already present.
 };
 
 /**
  * SurfaceCache is an imagelib-global service that allows caching of temporary
@@ -136,18 +141,19 @@ enum class InsertOutcome : uint8_t {
  * objects, which hold surfaces but also layer on additional features specific
  * to imagelib's needs like animation, padding support, and transparent support
  * for volatile buffers.
  *
  * Sometime it's useful to temporarily prevent surfaces from expiring from the
  * cache. This is most often because losing the data could harm the user
  * experience (for example, we often don't want to allow surfaces that are
  * currently visible to expire) or because it's not possible to rematerialize
- * the surface. SurfaceCache supports this through the use of image locking; see
- * the comments for Insert() and LockImage() for more details.
+ * the surface. SurfaceCache supports this through the use of image locking and
+ * surface lifetimes; see the comments for Insert() and LockImage() for more
+ * details.
  *
  * Any image which stores surfaces in the SurfaceCache *must* ensure that it
  * calls RemoveImage() before it is destroyed. See the comments for
  * RemoveImage() for more details.
  */
 struct SurfaceCache
 {
   typedef gfx::IntSize IntSize;
@@ -167,18 +173,18 @@ struct SurfaceCache
    * drawable reference to that imgFrame.
    *
    * If the image associated with the surface is locked, then the surface will
    * be locked before it is returned.
    *
    * If the imgFrame was found in the cache, but had stored its surface in a
    * volatile buffer which was discarded by the OS, then it is automatically
    * removed from the cache and an empty LookupResult is returned. Note that
-   * this will never happen to surfaces associated with a locked image; the
-   * cache keeps a strong reference to such surfaces internally.
+   * this will never happen to persistent surfaces associated with a locked
+   * image; the cache keeps a strong reference to such surfaces internally.
    *
    * @param aImageKey       Key data identifying which image the surface belongs
    *                        to.
    *
    * @param aSurfaceKey     Key data which uniquely identifies the requested
    *                        surface.
    *
    * @param aAlternateFlags If not Nothing(), a different set of flags than the
@@ -225,58 +231,64 @@ struct SurfaceCache
                                       const Maybe<SurfaceFlags>& aAlternateFlags
                                         = Nothing());
 
   /**
    * Insert a surface into the cache. If a surface with the same ImageKey and
    * SurfaceKey is already in the cache, Insert returns FAILURE_ALREADY_PRESENT.
    * If a matching placeholder is already present, the placeholder is removed.
    *
-   * Surfaces will never expire as long as they remain locked, but if they
-   * become unlocked, they can expire either because the SurfaceCache runs out
-   * of capacity or because they've gone too long without being used.  When it
-   * is first inserted, a surface is locked if its associated image is locked.
-   * When that image is later unlocked, the surface becomes unlocked too. To
-   * become locked again at that point, two things must happen: the image must
-   * become locked again (via LockImage()), and the surface must be touched
-   * again (via one of the Lookup() functions).
+   * Each surface in the cache has a lifetime, either Transient or Persistent.
+   * Transient surfaces can expire from the cache at any time. Persistent
+   * surfaces, on the other hand, will never expire as long as they remain
+   * locked, but if they become unlocked, can expire just like transient
+   * surfaces. When it is first inserted, a persistent surface is locked if its
+   * associated image is locked. When that image is later unlocked, the surface
+   * becomes unlocked too. To become locked again at that point, two things must
+   * happen: the image must become locked again (via LockImage()), and the
+   * surface must be touched again (via one of the Lookup() functions).
    *
    * All of this means that a very particular procedure has to be followed for
    * surfaces which cannot be rematerialized. First, they must be inserted
-   * *after* the image is locked with LockImage(); if you use the other order,
-   * the surfaces might expire before LockImage() gets called or before the
-   * surface is touched again by Lookup(). Second, the image they are associated
-   * with must never be unlocked.
+   * with a persistent lifetime *after* the image is locked with LockImage(); if
+   * you use the other order, the surfaces might expire before LockImage() gets
+   * called or before the surface is touched again by Lookup(). Second, the
+   * image they are associated with must never be unlocked.
    *
    * If a surface cannot be rematerialized, it may be important to know whether
    * it was inserted into the cache successfully. Insert() returns FAILURE if it
    * failed to insert the surface, which could happen because of capacity
-   * reasons, or because it was already freed by the OS. If the surface isn't
-   * associated with a locked image, checking for SUCCESS or FAILURE is useless:
-   * the surface might expire immediately after being inserted, even though
-   * Insert() returned SUCCESS. Thus, many callers do not need to check the
-   * result of Insert() at all.
+   * reasons, or because it was already freed by the OS. If you aren't inserting
+   * a surface with persistent lifetime, or if the surface isn't associated with
+   * a locked image, checking for SUCCESS or FAILURE is useless: the surface
+   * might expire immediately after being inserted, even though Insert()
+   * returned SUCCESS. Thus, many callers do not need to check the result of
+   * Insert() at all.
    *
    * @param aTarget      The new surface (wrapped in an imgFrame) to insert into
    *                     the cache.
    * @param aImageKey    Key data identifying which image the surface belongs
    *                     to.
    * @param aSurfaceKey  Key data which uniquely identifies the requested
    *                     surface.
+   * @param aLifetime    Whether this is a transient surface that can always be
+   *                     allowed to expire, or a persistent surface that
+   *                     shouldn't expire if the image is locked.
    * @return SUCCESS if the surface was inserted successfully. (But see above
    *           for more information about when you should check this.)
    *         FAILURE if the surface could not be inserted, e.g. for capacity
    *           reasons. (But see above for more information about when you
    *           should check this.)
    *         FAILURE_ALREADY_PRESENT if a surface with the same ImageKey and
    *           SurfaceKey already exists in the cache.
    */
   static InsertOutcome Insert(imgFrame*         aSurface,
                               const ImageKey    aImageKey,
-                              const SurfaceKey& aSurfaceKey);
+                              const SurfaceKey& aSurfaceKey,
+                              Lifetime          aLifetime);
 
   /**
    * Insert a placeholder for a surface into the cache. If a surface with the
    * same ImageKey and SurfaceKey is already in the cache, InsertPlaceholder()
    * returns FAILURE_ALREADY_PRESENT.
    *
    * Placeholders exist to allow lazy allocation of surfaces. The Lookup*()
    * methods will report whether a placeholder for an exactly matching surface
@@ -316,26 +328,27 @@ struct SurfaceCache
    *                        images.
    *
    * @return false if the surface cache can't hold a surface of that size.
    */
   static bool CanHold(const IntSize& aSize, uint32_t aBytesPerPixel = 4);
   static bool CanHold(size_t aSize);
 
   /**
-   * Locks an image. Any of the image's surfaces which are either inserted or
-   * accessed while the image is locked will not expire.
+   * Locks an image. Any of the image's persistent surfaces which are either
+   * inserted or accessed while the image is locked will not expire.
    *
    * Locking an image does not automatically lock that image's existing
-   * surfaces. A call to LockImage() guarantees that surfaces which are inserted
-   * afterward will not expire before the next call to UnlockImage() or
-   * UnlockSurfaces() for that image. Surfaces that are accessed via Lookup() or
-   * LookupBestMatch() after a LockImage() call will also not expire until the
-   * next UnlockImage() or UnlockSurfaces() call for that image. Any other
-   * surfaces owned by the image may expire at any time.
+   * surfaces. A call to LockImage() guarantees that persistent surfaces which
+   * are inserted afterward will not expire before the next call to
+   * UnlockImage() or UnlockSurfaces() for that image. Surfaces that are
+   * accessed via Lookup() or LookupBestMatch() after a LockImage() call will
+   * also not expire until the next UnlockImage() or UnlockSurfaces() call for
+   * that image. Any other surfaces owned by the image may expire at any time,
+   * whether they are persistent or transient.
    *
    * Regardless of locking, any of an image's surfaces may be removed using
    * RemoveSurface(), and all of an image's surfaces are removed by
    * RemoveImage(), whether the image is locked or not.
    *
    * It's safe to call LockImage() on an image that's already locked; this has
    * no effect.
    *
@@ -363,20 +376,20 @@ struct SurfaceCache
    * Unlocks the existing surfaces of an image, allowing them to expire at any
    * time.
    *
    * This does not unlock the image itself, so accessing the surfaces via
    * Lookup() or LookupBestMatch() will lock them again, and prevent them from
    * expiring.
    *
    * This is intended to be used in situations where it's no longer clear that
-   * all of the surfaces owned by an image are needed. Calling UnlockSurfaces()
-   * and then taking some action that will cause Lookup() to touch any surfaces
-   * that are still useful will permit the remaining surfaces to expire from the
-   * cache.
+   * all of the persistent surfaces owned by an image are needed. Calling
+   * UnlockSurfaces() and then taking some action that will cause Lookup() to
+   * touch any surfaces that are still useful will permit the remaining surfaces
+   * to expire from the cache.
    *
    * If the image is unlocked, this has no effect.
    *
    * @param aImageKey    The image which should have its existing surfaces
    *                     unlocked.
    */
   static void UnlockSurfaces(const ImageKey aImageKey);
 
@@ -407,19 +420,19 @@ struct SurfaceCache
    *
    * @param aImageKey  The image which should be removed from the cache.
    */
   static void RemoveImage(const ImageKey aImageKey);
 
   /**
    * Evicts all evictable surfaces from the cache.
    *
-   * All surfaces are evictable except for surfaces associated with locked
-   * images. Non-evictable surfaces can only be removed by RemoveSurface() or
-   * RemoveImage().
+   * All surfaces are evictable except for persistent surfaces associated with
+   * locked images. Non-evictable surfaces can only be removed by
+   * RemoveSurface() or RemoveImage().
    */
   static void DiscardAll();
 
   /**
    * Collects an accounting of the surfaces contained in the SurfaceCache for
    * the given image, along with their size and various other metadata.
    *
    * This is intended for use with memory reporting.
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -915,17 +915,18 @@ VectorImage::CreateSurfaceAndShow(const 
   if (!surface) {
     return Show(svgDrawable, aParams);
   }
 
   // Attempt to cache the frame.
   SurfaceCache::Insert(frame, ImageKey(this),
                        VectorSurfaceKey(aParams.size,
                                         aParams.svgContext,
-                                        aParams.animationTime));
+                                        aParams.animationTime),
+                       Lifetime::Persistent);
 
   // Draw.
   nsRefPtr<gfxDrawable> drawable =
     new gfxSurfaceDrawable(surface, aParams.size);
   Show(drawable, aParams);
 
   // Send out an invalidation so that surfaces that are still in use get
   // re-locked. See the discussion of the UnlockSurfaces call above.
--- a/image/decoders/nsBMPDecoder.cpp
+++ b/image/decoders/nsBMPDecoder.cpp
@@ -59,16 +59,30 @@ nsBMPDecoder::nsBMPDecoder(RasterImage* 
 nsBMPDecoder::~nsBMPDecoder()
 {
   delete[] mColors;
   if (mRow) {
       free(mRow);
   }
 }
 
+nsresult
+nsBMPDecoder::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 // Sets whether or not the BMP will use alpha data
 void
 nsBMPDecoder::SetUseAlphaData(bool useAlphaData)
 {
   mUseAlphaData = useAlphaData;
 }
 
 // Obtains the bits per pixel from the internal BIH header
--- a/image/decoders/nsBMPDecoder.h
+++ b/image/decoders/nsBMPDecoder.h
@@ -4,31 +4,34 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 #ifndef mozilla_image_decoders_nsBMPDecoder_h
 #define mozilla_image_decoders_nsBMPDecoder_h
 
 #include "BMPFileHeaders.h"
 #include "Decoder.h"
+#include "Downscaler.h"
 #include "gfxColor.h"
 #include "nsAutoPtr.h"
 
 namespace mozilla {
 namespace image {
 
 class RasterImage;
 
 /// Decoder for BMP-Files, as used by Windows and OS/2
 
 class nsBMPDecoder : public Decoder
 {
 public:
     ~nsBMPDecoder();
 
+    nsresult SetTargetSize(const nsIntSize& aSize) override;
+
     // Specifies whether or not the BMP file will contain alpha data
     // If set to true and the BMP is 32BPP, the alpha data will be
     // retrieved from the 4th byte of image data per pixel
     void SetUseAlphaData(bool useAlphaData);
 
     // Obtains the bits per pixel from the internal BIH header
     int32_t GetBitsPerPixel() const;
 
@@ -69,16 +72,18 @@ private:
 
     uint32_t mPos; //< Number of bytes read from aBuffer in WriteInternal()
 
     BMPFILEHEADER mBFH;
     BITMAPV5HEADER mBIH;
     char mRawBuf[BIH_INTERNAL_LENGTH::WIN_V3]; //< If this is changed,
                                                // WriteInternal() MUST be updated
 
+    Maybe<Downscaler> mDownscaler;
+
     uint32_t mLOH; //< Length of the header
 
     uint32_t mNumColors; //< The number of used colors, i.e. the number of
                          // entries in mColors
     colorTable* mColors;
 
     bitFields mBitFields;
 
--- a/image/decoders/nsGIFDecoder2.cpp
+++ b/image/decoders/nsGIFDecoder2.cpp
@@ -94,16 +94,30 @@ nsGIFDecoder2::nsGIFDecoder2(RasterImage
 }
 
 nsGIFDecoder2::~nsGIFDecoder2()
 {
   free(mGIFStruct.local_colormap);
   free(mGIFStruct.hold);
 }
 
+nsresult
+nsGIFDecoder2::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 uint8_t*
 nsGIFDecoder2::GetCurrentRowBuffer()
 {
   if (!mDownscaler) {
     MOZ_ASSERT(!mDeinterlacer, "Deinterlacer without downscaler?");
     uint32_t bpp = mGIFStruct.images_decoded == 0 ? sizeof(uint32_t)
                                                   : sizeof(uint8_t);
     return mImageData + mGIFStruct.irow * mGIFStruct.width * bpp;
--- a/image/decoders/nsGIFDecoder2.h
+++ b/image/decoders/nsGIFDecoder2.h
@@ -3,32 +3,35 @@
  * 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_decoders_nsGIFDecoder2_h
 #define mozilla_image_decoders_nsGIFDecoder2_h
 
 #include "Decoder.h"
+#include "Downscaler.h"
 
 #include "GIF2.h"
 #include "nsCOMPtr.h"
 
 namespace mozilla {
 namespace image {
 class RasterImage;
 
 //////////////////////////////////////////////////////////////////////
 // nsGIFDecoder2 Definition
 
 class nsGIFDecoder2 : public Decoder
 {
 public:
   ~nsGIFDecoder2();
 
+  nsresult SetTargetSize(const nsIntSize& aSize) override;
+
   virtual void WriteInternal(const char* aBuffer, uint32_t aCount) override;
   virtual void FinishInternal() override;
   virtual Telemetry::ID SpeedHistogram() override;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
@@ -65,15 +68,16 @@ private:
 
   uint8_t mCurrentPass;
   uint8_t mLastFlushedPass;
   uint8_t mColorMask;        // Apply this to the pixel to keep within colormap
   bool mGIFOpen;
   bool mSawTransparency;
 
   gif_struct mGIFStruct;
+  Maybe<Downscaler> mDownscaler;
   Maybe<Deinterlacer> mDeinterlacer;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_decoders_nsGIFDecoder2_h
--- a/image/decoders/nsICODecoder.cpp
+++ b/image/decoders/nsICODecoder.cpp
@@ -66,16 +66,30 @@ nsICODecoder::nsICODecoder(RasterImage* 
   , mBestResourceColorDepth(0)
   , mNumIcons(0)
   , mCurrIcon(0)
   , mBPP(0)
   , mMaskRowSize(0)
   , mCurrMaskLine(0)
 { }
 
+nsresult
+nsICODecoder::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 void
 nsICODecoder::FinishInternal()
 {
   // We shouldn't be called in error cases
   MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!");
 
   GetFinalStateFromContainedDecoder();
 }
--- a/image/decoders/nsICODecoder.h
+++ b/image/decoders/nsICODecoder.h
@@ -38,16 +38,18 @@ enum class ICOState
   FINISHED_RESOURCE
 };
 
 class nsICODecoder : public Decoder
 {
 public:
   virtual ~nsICODecoder() { }
 
+  nsresult SetTargetSize(const nsIntSize& aSize) override;
+
   /// @return the width of the icon directory entry @aEntry.
   static uint32_t GetRealWidth(const IconDirEntry& aEntry)
   {
     return aEntry.mWidth == 0 ? 256 : aEntry.mWidth;
   }
 
   /// @return the width of the selected directory entry (mDirEntry).
   uint32_t GetRealWidth() const { return GetRealWidth(mDirEntry); }
@@ -112,16 +114,17 @@ private:
   LexerTransition<ICOState> ReadPNG(const char* aData, uint32_t aLen);
   LexerTransition<ICOState> ReadBIH(const char* aData);
   LexerTransition<ICOState> ReadBMP(const char* aData, uint32_t aLen);
   LexerTransition<ICOState> PrepareForMask();
   LexerTransition<ICOState> ReadMaskRow(const char* aData);
   LexerTransition<ICOState> FinishResource();
 
   StreamingLexer<ICOState, 32> mLexer; // The lexer.
+  Maybe<Downscaler> mDownscaler;       // Our downscaler, if we're downscaling.
   nsRefPtr<Decoder> mContainedDecoder; // Either a BMP or PNG decoder.
   char mBIHraw[40];                    // The bitmap information header.
   IconDirEntry mDirEntry;              // The dir entry for the selected resource.
   IntSize mBiggestResourceSize;        // Used to select the intrinsic size.
   IntSize mBiggestResourceHotSpot;     // Used to select the intrinsic size.
   uint16_t mBiggestResourceColorDepth; // Used to select the intrinsic size.
   int32_t mBestResourceDelta;          // Used to select the best resource.
   uint16_t mBestResourceColorDepth;    // Used to select the best resource.
--- a/image/decoders/nsIconDecoder.cpp
+++ b/image/decoders/nsIconDecoder.cpp
@@ -28,16 +28,30 @@ nsIconDecoder::nsIconDecoder(RasterImage
  , mHeight(-1)
 {
   // Nothing to do
 }
 
 nsIconDecoder::~nsIconDecoder()
 { }
 
+nsresult
+nsIconDecoder::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 void
 nsIconDecoder::WriteInternal(const char* aBuffer, uint32_t aCount)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call WriteInternal after error!");
 
   // Loop until the input data is gone
   while (aCount > 0) {
     switch (mState) {
--- a/image/decoders/nsIconDecoder.h
+++ b/image/decoders/nsIconDecoder.h
@@ -34,24 +34,28 @@ class RasterImage;
 //
 ////////////////////////////////////////////////////////////////////////////////
 
 class nsIconDecoder : public Decoder
 {
 public:
   virtual ~nsIconDecoder();
 
+  virtual nsresult SetTargetSize(const nsIntSize& aSize) override;
+
   virtual void WriteInternal(const char* aBuffer, uint32_t aCount) override;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   explicit nsIconDecoder(RasterImage* aImage);
 
+  Maybe<Downscaler> mDownscaler;
+
   uint32_t mExpectedDataLength;
   uint32_t mPixBytesRead;
   uint32_t mState;
   uint8_t mWidth;
   uint8_t mHeight;
 };
 
 enum {
--- a/image/decoders/nsJPEGDecoder.cpp
+++ b/image/decoders/nsJPEGDecoder.cpp
@@ -131,16 +131,30 @@ nsJPEGDecoder::~nsJPEGDecoder()
 }
 
 Telemetry::ID
 nsJPEGDecoder::SpeedHistogram()
 {
   return Telemetry::IMAGE_DECODE_SPEED_JPEG;
 }
 
+nsresult
+nsJPEGDecoder::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 void
 nsJPEGDecoder::InitInternal()
 {
   mCMSMode = gfxPlatform::GetCMSMode();
   if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
     mCMSMode = eCMSMode_Off;
   }
 
--- a/image/decoders/nsJPEGDecoder.h
+++ b/image/decoders/nsJPEGDecoder.h
@@ -10,16 +10,17 @@
 #include "RasterImage.h"
 // On Windows systems, RasterImage.h brings in 'windows.h', which defines INT32.
 // But the jpeg decoder has its own definition of INT32. To avoid build issues,
 // we need to undefine the version from 'windows.h'.
 #undef INT32
 
 #include "Decoder.h"
 
+#include "Downscaler.h"
 #include "nsAutoPtr.h"
 
 #include "nsIInputStream.h"
 #include "nsIPipe.h"
 #include "qcms.h"
 
 extern "C" {
 #include "jpeglib.h"
@@ -49,32 +50,36 @@ typedef enum {
 class RasterImage;
 struct Orientation;
 
 class nsJPEGDecoder : public Decoder
 {
 public:
   virtual ~nsJPEGDecoder();
 
+  virtual nsresult SetTargetSize(const nsIntSize& aSize) override;
+
   virtual void SetSampleSize(int aSampleSize) override
   {
     mSampleSize = aSampleSize;
   }
 
   virtual void InitInternal() override;
   virtual void WriteInternal(const char* aBuffer, uint32_t aCount) override;
   virtual void FinishInternal() override;
 
   virtual Telemetry::ID SpeedHistogram() override;
   void NotifyDone();
 
 protected:
   Orientation ReadOrientationFromEXIF();
   void OutputScanlines(bool* suspend);
 
+  Maybe<Downscaler> mDownscaler;
+
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   nsJPEGDecoder(RasterImage* aImage, Decoder::DecodeStyle aDecodeStyle);
 
 public:
   struct jpeg_decompress_struct mInfo;
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -136,16 +136,30 @@ nsPNGDecoder::~nsPNGDecoder()
 
     // mTransform belongs to us only if mInProfile is non-null
     if (mTransform) {
       qcms_transform_release(mTransform);
     }
   }
 }
 
+nsresult
+nsPNGDecoder::SetTargetSize(const nsIntSize& aSize)
+{
+  // Make sure the size is reasonable.
+  if (MOZ_UNLIKELY(aSize.width <= 0 || aSize.height <= 0)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a downscaler that we'll filter our output through.
+  mDownscaler.emplace(aSize);
+
+  return NS_OK;
+}
+
 void
 nsPNGDecoder::CheckForTransparency(SurfaceFormat aFormat,
                                    const IntRect& aFrameRect)
 {
   // Check if the image has a transparent color in its palette.
   if (aFormat == SurfaceFormat::B8G8R8A8) {
     PostHasTransparency();
   }
--- a/image/decoders/nsPNGDecoder.h
+++ b/image/decoders/nsPNGDecoder.h
@@ -3,16 +3,17 @@
  * 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_decoders_nsPNGDecoder_h
 #define mozilla_image_decoders_nsPNGDecoder_h
 
 #include "Decoder.h"
+#include "Downscaler.h"
 
 #include "gfxTypes.h"
 
 #include "nsCOMPtr.h"
 
 #include "png.h"
 
 #include "qcms.h"
@@ -21,16 +22,18 @@ namespace mozilla {
 namespace image {
 class RasterImage;
 
 class nsPNGDecoder : public Decoder
 {
 public:
   virtual ~nsPNGDecoder();
 
+  virtual nsresult SetTargetSize(const nsIntSize& aSize) override;
+
   virtual void InitInternal() override;
   virtual void WriteInternal(const char* aBuffer, uint32_t aCount) override;
   virtual Telemetry::ID SpeedHistogram() override;
 
   nsresult CreateFrame(png_uint_32 aXOffset, png_uint_32 aYOffset,
                        int32_t aWidth, int32_t aHeight,
                        gfx::SurfaceFormat aFormat);
   void EndImageFrame();
@@ -77,16 +80,17 @@ private:
   // XXX(seth): nsICODecoder is temporarily an exception to this rule.
   explicit nsPNGDecoder(RasterImage* aImage);
 
   void PostPartialInvalidation(const IntRect& aInvalidRegion);
   void PostFullInvalidation();
 
 public:
   png_structp mPNG;
+  Maybe<Downscaler> mDownscaler;
   png_infop mInfo;
   nsIntRect mFrameRect;
   uint8_t* mCMSLine;
   uint8_t* interlacebuf;
   qcms_profile* mInProfile;
   qcms_transform* mTransform;
 
   gfx::SurfaceFormat format;
--- a/image/imgFrame.cpp
+++ b/image/imgFrame.cpp
@@ -791,18 +791,98 @@ imgFrame::LockImageData()
     return NS_OK;
   }
 
   // Paletted images don't have surfaces, so there's nothing to do.
   if (mPalettedImageData) {
     return NS_OK;
   }
 
-  MOZ_ASSERT_UNREACHABLE("It's illegal to re-lock an optimized imgFrame");
-  return NS_ERROR_FAILURE;
+  return Deoptimize();
+}
+
+nsresult
+imgFrame::Deoptimize()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mMonitor.AssertCurrentThreadOwns();
+  MOZ_ASSERT(!mImageSurface);
+
+  if (!mImageSurface) {
+    if (mVBuf) {
+      VolatileBufferPtr<uint8_t> ref(mVBuf);
+      if (ref.WasBufferPurged()) {
+        return NS_ERROR_FAILURE;
+      }
+
+      mImageSurface = CreateLockedSurface(mVBuf, mSize, mFormat);
+      if (!mImageSurface) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+    }
+    if (mOptSurface || mSinglePixel || mFormat == SurfaceFormat::R5G6B5) {
+      SurfaceFormat format = mFormat;
+      if (mFormat == SurfaceFormat::R5G6B5) {
+        format = SurfaceFormat::B8G8R8A8;
+      }
+
+      // Recover the pixels
+      RefPtr<VolatileBuffer> buf =
+        AllocateBufferForImage(mSize, format);
+      if (!buf) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+
+      RefPtr<DataSourceSurface> surf =
+        CreateLockedSurface(buf, mSize, format);
+      if (!surf) {
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+
+      DataSourceSurface::MappedSurface mapping;
+      if (!surf->Map(DataSourceSurface::MapType::WRITE, &mapping)) {
+        gfxCriticalError() << "imgFrame::Deoptimize failed to map surface";
+        return NS_ERROR_FAILURE;
+      }
+
+      RefPtr<DrawTarget> target =
+        Factory::CreateDrawTargetForData(BackendType::CAIRO,
+                                         mapping.mData,
+                                         mSize,
+                                         mapping.mStride,
+                                         format);
+      if (!target) {
+        gfxWarning() <<
+          "imgFrame::Deoptimize failed in CreateDrawTargetForData";
+        return NS_ERROR_OUT_OF_MEMORY;
+      }
+
+      Rect rect(0, 0, mSize.width, mSize.height);
+      if (mSinglePixel) {
+        target->FillRect(rect, ColorPattern(mSinglePixelColor),
+                         DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+      } else if (mFormat == SurfaceFormat::R5G6B5) {
+        target->DrawSurface(mImageSurface, rect, rect);
+      } else {
+        target->DrawSurface(mOptSurface, rect, rect,
+                            DrawSurfaceOptions(),
+                            DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+      }
+      target->Flush();
+      surf->Unmap();
+
+      mFormat = format;
+      mVBuf = buf;
+      mImageSurface = surf;
+      mOptSurface = nullptr;
+    }
+  }
+
+  mVBufPtr = mVBuf;
+  return NS_OK;
 }
 
 void
 imgFrame::AssertImageDataLocked() const
 {
 #ifdef DEBUG
   MonitorAutoLock lock(mMonitor);
   MOZ_ASSERT(mLockCount > 0, "Image data should be locked");
--- a/image/imgFrame.h
+++ b/image/imgFrame.h
@@ -269,16 +269,17 @@ public:
 
 private: // methods
 
   ~imgFrame();
 
   nsresult LockImageData();
   nsresult UnlockImageData();
   nsresult Optimize();
+  nsresult Deoptimize();
 
   void AssertImageDataLocked() const;
 
   bool IsImageCompleteInternal() const;
   nsresult ImageUpdatedInternal(const nsIntRect& aUpdateRect);
   void GetImageDataInternal(uint8_t** aData, uint32_t* length) const;
   uint32_t GetImageBytesPerRow() const;
   uint32_t GetImageDataLength() const;
@@ -442,19 +443,16 @@ private:
  * |ref| and |if (ref)| is true, then calls to GetImageData(), GetPaletteData(),
  * and GetDrawTarget() are guaranteed to succeed. This guarantee is stronger
  * than DrawableFrameRef, so everything that a valid DrawableFrameRef guarantees
  * is also guaranteed by a valid RawAccessFrameRef.
  *
  * This may be considerably more expensive than is necessary just for drawing,
  * so only use this when you need to read or write the raw underlying image data
  * that the imgFrame holds.
- *
- * Once all an imgFrame's RawAccessFrameRefs go out of scope, new
- * RawAccessFrameRefs cannot be created.
  */
 class RawAccessFrameRef final
 {
 public:
   RawAccessFrameRef() { }
 
   explicit RawAccessFrameRef(imgFrame* aFrame)
     : mFrame(aFrame)
--- a/image/test/reftest/downscaling/reftest.list
+++ b/image/test/reftest/downscaling/reftest.list
@@ -19,28 +19,28 @@
 # sufficient.
 #
 # Also note that Mac OS X has its own system-level downscaling algorithm, so
 # tests here may need Mac-specific "fuzzy-if(cocoaWidget,...)" annotations.
 # Similarly, modern versions of Windows have slightly different downscaling
 # behavior than other platforms, and may require "fuzzy-if(winWidget,...)".
 
 
-# RUN TESTS NOT AFFECTED BY DOWNSCALE-DURING-DECODE:
-# ==================================================
+# RUN TESTS NOT AFFECTED BY HIGH QUALITY DOWNSCALING:
+# ===================================================
 == downscale-svg-1a.html downscale-svg-1-ref.html?80
 fuzzy(80,468) == downscale-svg-1b.html downscale-svg-1-ref.html?72
 == downscale-svg-1c.html downscale-svg-1-ref.html?64
 fuzzy(17,208) fuzzy-if(B2G,255,207) == downscale-svg-1d.html downscale-svg-1-ref.html?53 # right side is 1 pixel off for B2G, probably regression from 974242
 fuzzy(78,216) == downscale-svg-1e.html downscale-svg-1-ref.html?40
 fuzzy(51,90) == downscale-svg-1f.html downscale-svg-1-ref.html?24
 
-# RUN TESTS WITH DOWNSCALE-DURING-DECODE DISABLED:
-# ================================================
-default-preferences pref(image.downscale-during-decode.enabled,false)
+# RUN TESTS WITH HIGH QUALITY DOWNSCALING DISABLED:
+# =================================================
+default-preferences pref(image.high_quality_downscaling.enabled,false)
 
 fuzzy-if(winWidget,16,20) fuzzy-if(cocoaWidget,106,31) == downscale-1.html downscale-1-ref.html
 
 fuzzy(20,999) != downscale-2a.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2b.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2c.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2d.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2e.html?203,52,left about:blank
@@ -85,19 +85,20 @@ fuzzy(20,999) != downscale-2a.html?205,5
 fuzzy(20,999) != downscale-2b.html?205,53,bottom about:blank
 fuzzy(20,999) != downscale-2c.html?205,53,bottom about:blank
 fuzzy(20,999) != downscale-2d.html?205,53,bottom about:blank
 fuzzy(20,999) fails-if(OSX>=1008) != downscale-2e.html?205,53,bottom about:blank
 
 == downscale-png.html?16,16,interlaced downscale-png.html?16,16,normal
 == downscale-png.html?24,24,interlaced downscale-png.html?24,24,normal
 
-# RUN TESTS WITH DOWNSCALE-DURING-DECODE ENABLED:
-# ===============================================
-default-preferences pref(image.downscale-during-decode.enabled,true)
+# RUN TESTS WITH HIGH QUALITY DOWNSCALING ENABLED:
+# ================================================
+# High-quality downscaling enabled:
+default-preferences pref(image.high_quality_downscaling.enabled,true)
 
 fuzzy(31,127) fuzzy-if(d2d,31,147) == downscale-1.html downscale-1-ref.html # intermittently 147 pixels on win7 accelerated only (not win8)
 
 fuzzy(20,999) != downscale-2a.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2b.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2c.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2d.html?203,52,left about:blank
 fuzzy(20,999) != downscale-2e.html?203,52,left about:blank
--- a/layout/reftests/pixel-rounding/reftest.list
+++ b/layout/reftests/pixel-rounding/reftest.list
@@ -119,17 +119,17 @@ fails == collapsed-border-top-6.html bor
 == image-height-top-4.html image-height-4.html
 == image-height-top-5.html image-height-5.html
 == image-height-top-6.html image-height-6.html
 == image-width-left-4.html image-width-4.html
 == image-width-left-5.html image-width-5.html
 == image-width-left-6.html image-width-6.html
 
 
-skip pref(image.downscale-during-decode.enabled,true) == image-high-quality-scaling-1.html image-high-quality-scaling-1-ref.html
+skip pref(image.high_quality_downscaling.enabled,true) == image-high-quality-scaling-1.html image-high-quality-scaling-1-ref.html
 
 
 != offscreen-0-ref.html offscreen-10-ref.html
 == offscreen-background-color-pos-4.html offscreen-0-ref.html
 == offscreen-background-color-pos-5.html offscreen-10-ref.html
 == offscreen-background-color-pos-6.html offscreen-10-ref.html
 == offscreen-background-color-size-4.html offscreen-0-ref.html
 == offscreen-background-color-size-5.html offscreen-10-ref.html
--- a/layout/tools/reftest/reftest-preferences.js
+++ b/layout/tools/reftest/reftest-preferences.js
@@ -18,18 +18,18 @@
     branch.setBoolPref("app.update.enabled", false);
     // Disable addon updates and prefetching so we don't leak them
     branch.setBoolPref("extensions.update.enabled", false);
     branch.setBoolPref("extensions.getAddons.cache.enabled", false);
     // Disable blocklist updates so we don't have them reported as leaks
     branch.setBoolPref("extensions.blocklist.enabled", false);
     // Make url-classifier updates so rare that they won't affect tests
     branch.setIntPref("urlclassifier.updateinterval", 172800);
-    // Disable downscale-during-decode, since it makes reftests more difficult.
-    branch.setBoolPref("image.downscale-during-decode.enabled", false);
+    // Disable high-quality downscaling, since it makes reftests more difficult.
+    branch.setBoolPref("image.high_quality_downscaling.enabled", false);
     // Disable the single-color optimization, since it can cause intermittent
     // oranges and it causes many of our tests to test a different code path
     // than the one that normal images on the web use.
     branch.setBoolPref("image.single-color-optimization.enabled", false);
     // Checking whether two files are the same is slow on Windows.
     // Setting this pref makes tests run much faster there.
     branch.setBoolPref("security.fileuri.strict_origin_policy", false);
     // Disable the thumbnailing service
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -64,16 +64,17 @@ pref("browser.cache.memory.enable", fals
 pref("browser.cache.memory.enable", true);
 #endif
 pref("browser.cache.memory.capacity", 1024); // kilobytes
 
 pref("browser.cache.memory_limit", 5120); // 5 MB
 
 /* image cache prefs */
 pref("image.cache.size", 1048576); // bytes
+pref("image.high_quality_downscaling.enabled", false);
 
 /* offline cache prefs */
 pref("browser.offline-apps.notify", true);
 pref("browser.cache.offline.enable", true);
 pref("browser.cache.offline.capacity", 5120); // kilobytes
 pref("offline-apps.quota.warn", 1024); // kilobytes
 
 // cache compression turned off for now - see bug #715198
--- a/mobile/android/b2gdroid/app/b2gdroid.js
+++ b/mobile/android/b2gdroid/app/b2gdroid.js
@@ -63,16 +63,17 @@ pref("browser.cache.memory.enable", fals
 pref("browser.cache.memory.enable", true);
 #endif
 pref("browser.cache.memory.capacity", 1024); // kilobytes
 
 pref("browser.cache.memory_limit", 5120); // 5 MB
 
 /* image cache prefs */
 pref("image.cache.size", 1048576); // bytes
+pref("image.high_quality_downscaling.enabled", false);
 
 /* offline cache prefs */
 pref("browser.offline-apps.notify", true);
 pref("browser.cache.offline.enable", true);
 pref("browser.cache.offline.capacity", 5120); // kilobytes
 pref("offline-apps.quota.warn", 1024); // kilobytes
 
 // cache compression turned off for now - see bug #715198
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4064,16 +4064,26 @@ pref("image.cache.timeweight", 500);
 pref("image.decode-immediately.enabled", false);
 
 // Whether we attempt to downscale images during decoding.
 pref("image.downscale-during-decode.enabled", true);
 
 // The default Accept header sent for images loaded over HTTP(S)
 pref("image.http.accept", "image/png,image/*;q=0.8,*/*;q=0.5");
 
+pref("image.high_quality_downscaling.enabled", true);
+
+// The minimum percent downscaling we'll use high-quality downscaling on,
+// interpreted as a floating-point number / 1000.
+pref("image.high_quality_downscaling.min_factor", 1000);
+
+// The maximum memory size which we'll use high-quality uspcaling on,
+// interpreted as number of decoded bytes.
+pref("image.high_quality_upscaling.max_size", 20971520);
+
 // The threshold for inferring that changes to an <img> element's |src|
 // attribute by JavaScript represent an animation, in milliseconds. If the |src|
 // attribute is changing more frequently than this value, then we enter a
 // special "animation mode" which is designed to eliminate flicker. Set to 0 to
 // disable.
 pref("image.infer-src-animation.threshold-ms", 2000);
 
 // Should we optimize away the surfaces of single-color images?