Bug 1456558 - Part 3. Implement factor of 2 scaling support for SVGs in VectorImage. r=tnikkel
☠☠ backed out by fac7810a8f31 ☠ ☠
authorAndrew Osmond <aosmond@mozilla.com>
Thu, 20 Sep 2018 18:15:34 -0400
changeset 437544 70d8f11cf6e8278b8e1509a69bd02adc0168e606
parent 437543 af9fc3daf97ca1ef1fe09587553deae1fa9c279c
child 437545 fac7810a8f317798d8d78e6cdd4034bf18385001
push id108085
push useraosmond@gmail.com
push dateThu, 20 Sep 2018 22:15:49 +0000
treeherdermozilla-inbound@70d8f11cf6e8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1456558
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1456558 - Part 3. Implement factor of 2 scaling support for SVGs in VectorImage. r=tnikkel If FLAG_HIGH_QUALITY_SCALING is used, we should use SurfaceCache::LookupBestMatch just like how it is done in RasterImage. This may provide an alternative size at which we should rasterize the SVG instead of the requested size. Since SurfaceCache imposes a maximum size for which it will permit rasterized SVGs, we should also bypass the cache entirely if we are well above that and simply draw directly to the draw target in such cases. With WebRender, it is somewhat more complicated. We will now return NOT_SUPPORTED if the size is too big, and this should trigger fallback to blob images. This should only produce drawing commands for the relevant region and save us the high cost of rasterized a very large surface on the main thread, which at the same time, looking as crisp as a user would expect.
image/Image.cpp
image/Image.h
image/LookupResult.h
image/SVGDrawingParameters.h
image/VectorImage.cpp
image/VectorImage.h
--- a/image/Image.cpp
+++ b/image/Image.cpp
@@ -155,16 +155,17 @@ ImageResource::GetImageContainerImpl(Lay
     }
   }
 
   if (container) {
     switch (entry->mLastDrawResult) {
       case ImgDrawResult::SUCCESS:
       case ImgDrawResult::BAD_IMAGE:
       case ImgDrawResult::BAD_ARGS:
+      case ImgDrawResult::NOT_SUPPORTED:
         container.forget(aOutContainer);
         return entry->mLastDrawResult;
       case ImgDrawResult::NOT_READY:
       case ImgDrawResult::INCOMPLETE:
       case ImgDrawResult::TEMPORARY_ERROR:
         // Temporary conditions where we need to rerequest the frame to recover.
         break;
       case ImgDrawResult::WRONG_SIZE:
@@ -213,16 +214,17 @@ ImageResource::GetImageContainerImpl(Lay
       if (bestSize == entry->mSize && flags == entry->mFlags &&
           aSVGContext == entry->mSVGContext) {
         container = entry->mContainer.get();
         if (container) {
           switch (entry->mLastDrawResult) {
             case ImgDrawResult::SUCCESS:
             case ImgDrawResult::BAD_IMAGE:
             case ImgDrawResult::BAD_ARGS:
+            case ImgDrawResult::NOT_SUPPORTED:
               container.forget(aOutContainer);
               return entry->mLastDrawResult;
             case ImgDrawResult::NOT_READY:
             case ImgDrawResult::INCOMPLETE:
             case ImgDrawResult::TEMPORARY_ERROR:
               // Temporary conditions where we need to rerequest the frame to
               // recover. We have already done so!
               break;
--- a/image/Image.h
+++ b/image/Image.h
@@ -332,16 +332,34 @@ protected:
   TimeStamp                     mLastRefreshTime;
   uint64_t                      mInnerWindowId;
   uint32_t                      mAnimationConsumers;
   uint16_t                      mAnimationMode; // Enum values in imgIContainer
   bool                          mInitialized:1; // Have we been initalized?
   bool                          mAnimating:1;   // Are we currently animating?
   bool                          mError:1;       // Error handling
 
+  /**
+   * Attempt to find a matching cached surface in the SurfaceCache, and if not
+   * available, request the production of such a surface (either synchronously
+   * or asynchronously).
+   *
+   * If the draw result is BAD_IMAGE, BAD_ARGS or NOT_READY, the size will be
+   * the same as aSize. If it is TEMPORARY_ERROR, INCOMPLETE, or SUCCESS, the
+   * size is a hint as to what we expect the surface size to be, once the best
+   * fitting size is available. It may or may not match the size of the surface
+   * returned at this moment. This is useful for choosing how to store the final
+   * result (e.g. if going into an ImageContainer, ideally we would share the
+   * same container for many requested sizes, if they all end up with the same
+   * best fit size in the end).
+   *
+   * A valid surface should only be returned for SUCCESS and INCOMPLETE.
+   *
+   * Any other draw result is invalid.
+   */
   virtual Tuple<ImgDrawResult, gfx::IntSize, RefPtr<gfx::SourceSurface>>
     GetFrameInternal(const gfx::IntSize& aSize,
                      const Maybe<SVGImageContext>& aSVGContext,
                      uint32_t aWhichFrame,
                      uint32_t aFlags)
   {
     return MakeTuple(ImgDrawResult::BAD_IMAGE, aSize,
                      RefPtr<gfx::SourceSurface>());
--- a/image/LookupResult.h
+++ b/image/LookupResult.h
@@ -104,17 +104,20 @@ public:
 
 private:
   LookupResult(const LookupResult&) = delete;
   LookupResult& operator=(const LookupResult& aOther) = delete;
 
   DrawableSurface mSurface;
   MatchType mMatchType;
 
-  /// If given, the size the caller should request a decode at. This may or may
-  /// not match the size the caller requested from the cache.
+  /// mSuggestedSize will be the size of the returned surface if the result is
+  /// SUBSTITUTE_BECAUSE_BEST. It will be empty for EXACT, and can contain a
+  /// non-empty size possibly different from the returned surface (if any) for
+  /// all other results. If non-empty, it will always be the size the caller
+  /// should request any decodes at.
   gfx::IntSize mSuggestedSize;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_LookupResult_h
--- a/image/SVGDrawingParameters.h
+++ b/image/SVGDrawingParameters.h
@@ -19,43 +19,46 @@ namespace mozilla {
 namespace image {
 
 struct SVGDrawingParameters
 {
   typedef mozilla::gfx::IntSize IntSize;
   typedef mozilla::gfx::SamplingFilter SamplingFilter;
 
   SVGDrawingParameters(gfxContext* aContext,
-                       const nsIntSize& aSize,
+                       const nsIntSize& aRasterSize,
+                       const nsIntSize& aDrawSize,
                        const ImageRegion& aRegion,
                        SamplingFilter aSamplingFilter,
                        const Maybe<SVGImageContext>& aSVGContext,
                        float aAnimationTime,
                        uint32_t aFlags,
                        float aOpacity)
     : context(aContext)
-    , size(aSize.width, aSize.height)
+    , size(aRasterSize)
+    , drawSize(aDrawSize)
     , region(aRegion)
     , samplingFilter(aSamplingFilter)
     , svgContext(aSVGContext)
-    , viewportSize(aSize)
+    , viewportSize(aRasterSize)
     , animationTime(aAnimationTime)
     , flags(aFlags)
     , opacity(aOpacity)
   {
     if (aSVGContext) {
       auto sz = aSVGContext->GetViewportSize();
       if (sz) {
         viewportSize = nsIntSize(sz->width, sz->height); // XXX losing unit
       }
     }
   }
 
   gfxContext*                   context;
-  IntSize                       size;
+  IntSize                       size; // Size to rasterize a surface at.
+  IntSize                       drawSize; // Size to draw the given surface at.
   ImageRegion                   region;
   SamplingFilter                samplingFilter;
   const Maybe<SVGImageContext>& svgContext;
   nsIntSize                     viewportSize;
   float                         animationTime;
   uint32_t                      flags;
   gfxFloat                      opacity;
 };
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -802,55 +802,62 @@ VectorImage::GetFrameInternal(const IntS
                      RefPtr<SourceSurface>());
   }
 
   if (!mIsFullyLoaded) {
     return MakeTuple(ImgDrawResult::NOT_READY, aSize,
                      RefPtr<SourceSurface>());
   }
 
-  RefPtr<SourceSurface> sourceSurface =
+  // We don't allow large surfaces to be rasterized on the Draw and
+  // GetImageContainerAtSize paths, because those have alternatives. If we get
+  // here however, then we know it came from GetFrame(AtSize) and that path does
+  // not have any fallback method, so we don't check UseSurfaceCacheForSize.
+  RefPtr<SourceSurface> sourceSurface;
+  IntSize decodeSize;
+  Tie(sourceSurface, decodeSize) =
     LookupCachedSurface(aSize, aSVGContext, aFlags);
   if (sourceSurface) {
-    return MakeTuple(ImgDrawResult::SUCCESS, aSize, std::move(sourceSurface));
+    return MakeTuple(ImgDrawResult::SUCCESS, decodeSize, std::move(sourceSurface));
   }
 
   if (mIsDrawing) {
     NS_WARNING("Refusing to make re-entrant call to VectorImage::Draw");
-    return MakeTuple(ImgDrawResult::TEMPORARY_ERROR, aSize,
+    return MakeTuple(ImgDrawResult::TEMPORARY_ERROR, decodeSize,
                      RefPtr<SourceSurface>());
   }
 
   // By using a null gfxContext, we ensure that we will always attempt to
   // create a surface, even if we aren't capable of caching it (e.g. due to our
   // flags, having an animation, etc). Otherwise CreateSurface will assume that
   // the caller is capable of drawing directly to its own draw target if we
   // cannot cache.
-  SVGDrawingParameters params(nullptr, aSize, ImageRegion::Create(aSize),
+  SVGDrawingParameters params(nullptr, decodeSize, aSize,
+                              ImageRegion::Create(decodeSize),
                               SamplingFilter::POINT, aSVGContext,
                               mSVGDocumentWrapper->GetCurrentTime(),
                               aFlags, 1.0);
 
   bool didCache; // Was the surface put into the cache?
   bool contextPaint = aSVGContext && aSVGContext->GetContextPaint();
 
   AutoRestoreSVGState autoRestore(params, mSVGDocumentWrapper,
                                   mIsDrawing, contextPaint);
 
   RefPtr<gfxDrawable> svgDrawable = CreateSVGDrawable(params);
   RefPtr<SourceSurface> surface =
     CreateSurface(params, svgDrawable, didCache);
   if (!surface) {
     MOZ_ASSERT(!didCache);
-    return MakeTuple(ImgDrawResult::TEMPORARY_ERROR, aSize,
+    return MakeTuple(ImgDrawResult::TEMPORARY_ERROR, decodeSize,
                      RefPtr<SourceSurface>());
   }
 
   SendFrameComplete(didCache, params.flags);
-  return MakeTuple(ImgDrawResult::SUCCESS, aSize, std::move(surface));
+  return MakeTuple(ImgDrawResult::SUCCESS, decodeSize, std::move(surface));
 }
 
 //******************************************************************************
 Tuple<ImgDrawResult, IntSize>
 VectorImage::GetImageContainerSize(LayerManager* aManager,
                                    const IntSize& aSize,
                                    uint32_t aFlags)
 {
@@ -899,36 +906,39 @@ VectorImage::GetImageContainer(LayerMana
 //******************************************************************************
 NS_IMETHODIMP_(bool)
 VectorImage::IsImageContainerAvailableAtSize(LayerManager* aManager,
                                              const IntSize& aSize,
                                              uint32_t aFlags)
 {
   // Since we only support image containers with WebRender, and it can handle
   // textures larger than the hw max texture size, we don't need to check aSize.
-  return !aSize.IsEmpty() && IsImageContainerAvailable(aManager, aFlags);
+  return !aSize.IsEmpty() &&
+         UseSurfaceCacheForSize(aSize) &&
+         IsImageContainerAvailable(aManager, aFlags);
 }
 
 //******************************************************************************
 NS_IMETHODIMP_(ImgDrawResult)
 VectorImage::GetImageContainerAtSize(layers::LayerManager* aManager,
                                      const gfx::IntSize& aSize,
                                      const Maybe<SVGImageContext>& aSVGContext,
                                      uint32_t aFlags,
                                      layers::ImageContainer** aOutContainer)
 {
+  if (!UseSurfaceCacheForSize(aSize)) {
+    return ImgDrawResult::NOT_SUPPORTED;
+  }
+
   Maybe<SVGImageContext> newSVGContext;
   MaybeRestrictSVGContext(newSVGContext, aSVGContext, aFlags);
 
-  // Since we do not support high quality scaling with SVG, we mask it off so
-  // that container requests with and without it map to the same container.
-  // Similarly the aspect ratio flag was already handled as part of the SVG
-  // context restriction above.
-  uint32_t flags = aFlags & ~(FLAG_HIGH_QUALITY_SCALING |
-                              FLAG_FORCE_PRESERVEASPECTRATIO_NONE);
+  // The aspect ratio flag was already handled as part of the SVG context
+  // restriction above.
+  uint32_t flags = aFlags & ~(FLAG_FORCE_PRESERVEASPECTRATIO_NONE);
   return GetImageContainerImpl(aManager, aSize,
                                newSVGContext ? newSVGContext : aSVGContext,
                                flags, aOutContainer);
 }
 
 bool
 VectorImage::MaybeRestrictSVGContext(Maybe<SVGImageContext>& aNewSVGContext,
                                      const Maybe<SVGImageContext>& aSVGContext,
@@ -997,47 +1007,51 @@ VectorImage::Draw(gfxContext* aContext,
   if (!mIsFullyLoaded) {
     return ImgDrawResult::NOT_READY;
   }
 
   if (mAnimationConsumers == 0) {
     SendOnUnlockedDraw(aFlags);
   }
 
-  // We should always bypass the cache when using DrawTargetRecording because
-  // we prefer the drawing commands in general to the rasterized surface. This
-  // allows blob images to avoid rasterized SVGs with WebRender.
-  if (aContext->GetDrawTarget()->GetBackendType() == BackendType::RECORDING) {
+  // We should bypass the cache when:
+  // - We are using a DrawTargetRecording because we prefer the drawing commands
+  //   in general to the rasterized surface. This allows blob images to avoid
+  //   rasterized SVGs with WebRender.
+  // - The size exceeds what we are will to cache as a rasterized surface.
+  if (aContext->GetDrawTarget()->GetBackendType() == BackendType::RECORDING ||
+      !UseSurfaceCacheForSize(aSize)) {
     aFlags |= FLAG_BYPASS_SURFACE_CACHE;
   }
 
   MOZ_ASSERT(!(aFlags & FLAG_FORCE_PRESERVEASPECTRATIO_NONE) ||
              (aSVGContext && aSVGContext->GetViewportSize()),
              "Viewport size is required when using "
              "FLAG_FORCE_PRESERVEASPECTRATIO_NONE");
 
   float animTime = (aWhichFrame == FRAME_FIRST)
                      ? 0.0f : mSVGDocumentWrapper->GetCurrentTime();
 
   Maybe<SVGImageContext> newSVGContext;
   bool contextPaint =
     MaybeRestrictSVGContext(newSVGContext, aSVGContext, aFlags);
 
-  SVGDrawingParameters params(aContext, aSize, aRegion, aSamplingFilter,
+  SVGDrawingParameters params(aContext, aSize, aSize, aRegion, aSamplingFilter,
                               newSVGContext ? newSVGContext : aSVGContext,
                               animTime, aFlags, aOpacity);
 
   // If we have an prerasterized version of this image that matches the
   // drawing parameters, use that.
-  RefPtr<SourceSurface> sourceSurface =
+  RefPtr<SourceSurface> sourceSurface;
+  Tie(sourceSurface, params.size) =
     LookupCachedSurface(aSize, params.svgContext, aFlags);
   if (sourceSurface) {
-    RefPtr<gfxDrawable> svgDrawable =
-      new gfxSurfaceDrawable(sourceSurface, sourceSurface->GetSize());
-    Show(svgDrawable, params);
+    RefPtr<gfxDrawable> drawable =
+      new gfxSurfaceDrawable(sourceSurface, params.size);
+    Show(drawable, params);
     return ImgDrawResult::SUCCESS;
   }
 
   // else, we need to paint the image:
 
   if (mIsDrawing) {
     NS_WARNING("Refusing to make re-entrant call to VectorImage::Draw");
     return ImgDrawResult::TEMPORARY_ERROR;
@@ -1071,50 +1085,77 @@ VectorImage::CreateSVGDrawable(const SVG
                            aParams.size,
                            aParams.flags);
 
   RefPtr<gfxDrawable> svgDrawable =
     new gfxCallbackDrawable(cb, aParams.size);
   return svgDrawable.forget();
 }
 
-already_AddRefed<SourceSurface>
+bool
+VectorImage::UseSurfaceCacheForSize(const IntSize& aSize) const
+{
+  int32_t maxSizeKB = gfxPrefs::ImageCacheMaxRasterizedSVGThresholdKB();
+  if (maxSizeKB <= 0) {
+    return true;
+  }
+
+  if (!SurfaceCache::IsLegalSize(aSize)) {
+    return false;
+  }
+
+  // With factor of 2 mode, we should be willing to use a surface which is up
+  // to twice the width, and twice the height, of the maximum sized surface
+  // before switching to drawing to the target directly. That means the size in
+  // KB works out to be:
+  //   width * height * 4 [bytes/pixel] * / 1024 [bytes/KB] <= 2 * 2 * maxSizeKB
+  return aSize.width * aSize.height / 1024 <= maxSizeKB;
+}
+
+Tuple<RefPtr<SourceSurface>, IntSize>
 VectorImage::LookupCachedSurface(const IntSize& aSize,
                                  const Maybe<SVGImageContext>& aSVGContext,
                                  uint32_t aFlags)
 {
   // If we're not allowed to use a cached surface, don't attempt a lookup.
   if (aFlags & FLAG_BYPASS_SURFACE_CACHE) {
-    return nullptr;
+    return MakeTuple(RefPtr<SourceSurface>(), aSize);
   }
 
   // We don't do any caching if we have animation, so don't bother with a lookup
   // in this case either.
   if (mHaveAnimations) {
-    return nullptr;
+    return MakeTuple(RefPtr<SourceSurface>(), aSize);
   }
 
-  LookupResult result =
-    SurfaceCache::Lookup(ImageKey(this),
-                         VectorSurfaceKey(aSize, aSVGContext));
+  LookupResult result(MatchType::NOT_FOUND);
+  SurfaceKey surfaceKey = VectorSurfaceKey(aSize, aSVGContext);
+  if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) {
+    result = SurfaceCache::Lookup(ImageKey(this), surfaceKey);
+  } else {
+    result = SurfaceCache::LookupBestMatch(ImageKey(this), surfaceKey);
+  }
 
-  MOZ_ASSERT(result.SuggestedSize().IsEmpty(), "SVG should not substitute!");
-  if (!result) {
-    return nullptr;  // No matching surface, or the OS freed the volatile buffer.
+  IntSize rasterSize = result.SuggestedSize().IsEmpty()
+                       ? aSize : result.SuggestedSize();
+  MOZ_ASSERT(result.Type() != MatchType::SUBSTITUTE_BECAUSE_PENDING);
+  if (!result || result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND) {
+    // No matching surface, or the OS freed the volatile buffer.
+    return MakeTuple(RefPtr<SourceSurface>(), rasterSize);
   }
 
   RefPtr<SourceSurface> sourceSurface = result.Surface()->GetSourceSurface();
   if (!sourceSurface) {
     // Something went wrong. (Probably a GPU driver crash or device reset.)
     // Attempt to recover.
     RecoverFromLossOfSurfaces();
-    return nullptr;
+    return MakeTuple(RefPtr<SourceSurface>(), rasterSize);
   }
 
-  return sourceSurface.forget();
+  return MakeTuple(std::move(sourceSurface), rasterSize);
 }
 
 already_AddRefed<SourceSurface>
 VectorImage::CreateSurface(const SVGDrawingParameters& aParams,
                            gfxDrawable* aSVGDrawable,
                            bool& aWillCache)
 {
   MOZ_ASSERT(mIsDrawing);
@@ -1184,17 +1225,25 @@ VectorImage::CreateSurface(const SVGDraw
   if (!aWillCache) {
     return surface.forget();
   }
 
   // Attempt to cache the frame.
   SurfaceKey surfaceKey = VectorSurfaceKey(aParams.size, aParams.svgContext);
   NotNull<RefPtr<ISurfaceProvider>> provider =
     MakeNotNull<SimpleSurfaceProvider*>(ImageKey(this), surfaceKey, frame);
-  SurfaceCache::Insert(provider);
+
+  if (SurfaceCache::Insert(provider) == InsertOutcome::SUCCESS &&
+      aParams.size != aParams.drawSize) {
+    // We created a new surface that wasn't the size we requested, which means
+    // we entered factor-of-2 mode. We should purge any surfaces we no longer
+    // need rather than waiting for the cache to expire them.
+    SurfaceCache::PruneImage(ImageKey(this));
+  }
+
   return surface.forget();
 }
 
 void
 VectorImage::SendFrameComplete(bool aDidCache, uint32_t aFlags)
 {
   // If the cache was not updated, we have nothing to do.
   if (!aDidCache) {
@@ -1219,20 +1268,31 @@ VectorImage::SendFrameComplete(bool aDid
     }));
   }
 }
 
 
 void
 VectorImage::Show(gfxDrawable* aDrawable, const SVGDrawingParameters& aParams)
 {
+  // The surface size may differ from the size at which we wish to draw. As
+  // such, we may need to adjust the context/region to take this into account.
+  gfxContextMatrixAutoSaveRestore saveMatrix(aParams.context);
+  ImageRegion region(aParams.region);
+  if (aParams.drawSize != aParams.size) {
+    gfx::Size scale(double(aParams.drawSize.width) / aParams.size.width,
+                    double(aParams.drawSize.height) / aParams.size.height);
+    aParams.context->Multiply(gfxMatrix::Scaling(scale.width, scale.height));
+    region.Scale(1.0 / scale.width, 1.0 / scale.height);
+  }
+
   MOZ_ASSERT(aDrawable, "Should have a gfxDrawable by now");
   gfxUtils::DrawPixelSnapped(aParams.context, aDrawable,
                              SizeDouble(aParams.size),
-                             aParams.region,
+                             region,
                              SurfaceFormat::B8G8R8A8,
                              aParams.samplingFilter,
                              aParams.flags, aParams.opacity, false);
 
 #ifdef DEBUG
   NotifyDrawingObservers();
 #endif
 
--- a/image/VectorImage.h
+++ b/image/VectorImage.h
@@ -87,30 +87,37 @@ private:
                      uint32_t aWhichFrame,
                      uint32_t aFlags) override;
 
   Tuple<ImgDrawResult, IntSize>
     GetImageContainerSize(layers::LayerManager* aManager,
                           const IntSize& aSize,
                           uint32_t aFlags) override;
 
-  /// Attempt to find a matching cached surface in the SurfaceCache.
-  already_AddRefed<SourceSurface>
+  /**
+   * Attempt to find a matching cached surface in the SurfaceCache. Returns the
+   * cached surface, if found, and the size to rasterize at, if applicable.
+   * If we cannot rasterize, it will be the requested size to draw at (aSize).
+   */
+  Tuple<RefPtr<SourceSurface>, IntSize>
     LookupCachedSurface(const IntSize& aSize,
                         const Maybe<SVGImageContext>& aSVGContext,
                         uint32_t aFlags);
 
   bool MaybeRestrictSVGContext(Maybe<SVGImageContext>& aNewSVGContext,
                                const Maybe<SVGImageContext>& aSVGContext,
                                uint32_t aFlags);
 
   /// Create a gfxDrawable which callbacks into the SVG document.
   already_AddRefed<gfxDrawable>
     CreateSVGDrawable(const SVGDrawingParameters& aParams);
 
+  /// Returns true if we use the surface cache to store rasterized copies.
+  bool UseSurfaceCacheForSize(const IntSize& aSize) const;
+
   /// Rasterize the SVG into a surface. aWillCache will be set to whether or
   /// not the new surface was put into the cache.
   already_AddRefed<SourceSurface>
     CreateSurface(const SVGDrawingParameters& aParams,
                   gfxDrawable* aSVGDrawable,
                   bool& aWillCache);
 
   /// Send a frame complete notification if appropriate. Must be called only