Bug 1485512 - Try to express SVG filters as CSS filters when possible. r=mstange
authorNicolas Silva <nsilva@mozilla.com>
Fri, 02 Nov 2018 19:01:14 +0100
changeset 445101 d6df6b97822a459c5ccb86de14f4843613d03827
parent 445100 504f4815ad87249fc37fdfd50bcfe2abdabbc404
child 445102 eac3a6a42941e308e78be135d96b24973ace4c34
push id109683
push usernsilva@mozilla.com
push dateThu, 08 Nov 2018 14:19:23 +0000
treeherdermozilla-inbound@eb3117b5f4fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange
bugs1485512
milestone65.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 1485512 - Try to express SVG filters as CSS filters when possible. r=mstange Differential Revision: https://phabricator.services.mozilla.com/D8085
gfx/webrender_bindings/src/bindings.rs
gfx/webrender_bindings/webrender_ffi_generated.h
layout/painting/nsDisplayList.cpp
layout/painting/nsDisplayList.h
layout/svg/nsFilterInstance.cpp
layout/svg/nsFilterInstance.h
layout/svg/nsSVGIntegrationUtils.cpp
layout/svg/nsSVGIntegrationUtils.h
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -430,16 +430,18 @@ pub enum WrFilterOpType {
   Grayscale = 3,
   HueRotate = 4,
   Invert = 5,
   Opacity = 6,
   Saturate = 7,
   Sepia = 8,
   DropShadow = 9,
   ColorMatrix = 10,
+  SrgbToLinear = 11,
+  LinearToSrgb = 12,
 }
 
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct WrFilterOp {
     filter_type: WrFilterOpType,
     argument: c_float, // holds radius for DropShadow; value for other filters
     offset: LayoutVector2D, // only used for DropShadow
@@ -1827,16 +1829,18 @@ pub extern "C" fn wr_dp_push_stacking_co
             WrFilterOpType::Invert => FilterOp::Invert(c_filter.argument),
             WrFilterOpType::Opacity => FilterOp::Opacity(PropertyBinding::Value(c_filter.argument), c_filter.argument),
             WrFilterOpType::Saturate => FilterOp::Saturate(c_filter.argument),
             WrFilterOpType::Sepia => FilterOp::Sepia(c_filter.argument),
             WrFilterOpType::DropShadow => FilterOp::DropShadow(c_filter.offset,
                                                                c_filter.argument,
                                                                c_filter.color),
             WrFilterOpType::ColorMatrix => FilterOp::ColorMatrix(c_filter.matrix),
+            WrFilterOpType::SrgbToLinear => FilterOp::SrgbToLinear,
+            WrFilterOpType::LinearToSrgb => FilterOp::LinearToSrgb,
         }
     }).collect();
 
     let clip_node_id_ref = unsafe { clip_node_id.as_ref() };
     let clip_node_id = match clip_node_id_ref {
         Some(clip_node_id) => Some(unpack_clip_id(*clip_node_id, state.pipeline_id)),
         None => None,
     };
--- a/gfx/webrender_bindings/webrender_ffi_generated.h
+++ b/gfx/webrender_bindings/webrender_ffi_generated.h
@@ -248,16 +248,18 @@ enum class WrFilterOpType : uint32_t {
   Grayscale = 3,
   HueRotate = 4,
   Invert = 5,
   Opacity = 6,
   Saturate = 7,
   Sepia = 8,
   DropShadow = 9,
   ColorMatrix = 10,
+  SrgbToLinear = 11,
+  LinearToSrgb = 12,
 
   Sentinel /* this must be last for serialization purposes. */
 };
 
 enum class YuvColorSpace : uint32_t {
   Rec601 = 0,
   Rec709 = 1,
 
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -10374,26 +10374,20 @@ nsDisplayFilters::PaintAsLayer(nsDisplay
 static float
 ClampStdDeviation(float aStdDeviation)
 {
   // Cap software blur radius for performance reasons.
   return std::min(std::max(0.0f, aStdDeviation), 100.0f);
 }
 
 bool
-nsDisplayFilters::CreateWebRenderCommands(
-  mozilla::wr::DisplayListBuilder& aBuilder,
-  mozilla::wr::IpcResourceUpdateQueue& aResources,
-  const StackingContextHelper& aSc,
-  mozilla::layers::WebRenderLayerManager* aManager,
-  nsDisplayListBuilder* aDisplayListBuilder)
-{
-  // All CSS filters are supported by WebRender. SVG filters are not supported,
-  // those use NS_STYLE_FILTER_URL.
-  nsTArray<mozilla::wr::WrFilterOp> wrFilters;
+nsDisplayFilters::CreateWebRenderCSSFilters(nsTArray<mozilla::wr::WrFilterOp>& wrFilters)
+{
+  // All CSS filters are supported by WebRender. SVG filters are not fully supported,
+  // those use NS_STYLE_FILTER_URL and are handled separately.
   const nsTArray<nsStyleFilter>& filters = mFrame->StyleEffects()->mFilters;
   for (const nsStyleFilter& filter : filters) {
     switch (filter.GetType()) {
       case NS_STYLE_FILTER_BRIGHTNESS:
       case NS_STYLE_FILTER_CONTRAST:
       case NS_STYLE_FILTER_GRAYSCALE:
       case NS_STYLE_FILTER_INVERT:
       case NS_STYLE_FILTER_OPACITY:
@@ -10456,26 +10450,48 @@ nsDisplayFilters::CreateWebRenderCommand
         wrFilters.AppendElement(filterOp);
         break;
       }
       default:
         return false;
     }
   }
 
+  return true;
+}
+
+bool
+nsDisplayFilters::CreateWebRenderCommands(
+  mozilla::wr::DisplayListBuilder& aBuilder,
+  mozilla::wr::IpcResourceUpdateQueue& aResources,
+  const StackingContextHelper& aSc,
+  mozilla::layers::WebRenderLayerManager* aManager,
+  nsDisplayListBuilder* aDisplayListBuilder)
+{
   bool snap;
   float auPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel();
   nsRect displayBounds = GetBounds(aDisplayListBuilder, &snap);
-  auto bounds = LayoutDeviceRect::FromAppUnits(displayBounds, auPerDevPixel);
-  // NOTE(emilio): this clip is going to be intersected with the clip that's
-  // currently on the clip stack for this item.
-  //
-  // FIXME(emilio, bug 1486557): clipping to "bounds" isn't really necessary.
+  auto postFilterBounds = LayoutDeviceIntRect::Round(
+    LayoutDeviceRect::FromAppUnits(displayBounds, auPerDevPixel)
+  );
+  auto preFilterBounds = LayoutDeviceIntRect::Round(
+    LayoutDeviceRect::FromAppUnits(mBounds, auPerDevPixel)
+  );
+
+  nsTArray<mozilla::wr::WrFilterOp> wrFilters;
+  if (!CreateWebRenderCSSFilters(wrFilters) &&
+      !nsSVGIntegrationUtils::BuildWebRenderFilters(mFrame,
+                                                    preFilterBounds,
+                                                    wrFilters,
+                                                    postFilterBounds)) {
+    return false;
+  }
+
   wr::WrClipId clipId =
-    aBuilder.DefineClip(Nothing(), wr::ToRoundedLayoutRect(bounds));
+    aBuilder.DefineClip(Nothing(), wr::ToLayoutRect(postFilterBounds));
 
   float opacity = mFrame->StyleEffects()->mOpacity;
   StackingContextHelper sc(aSc,
                            GetActiveScrolledRoot(),
                            aBuilder,
                            wrFilters,
                            LayoutDeviceRect(),
                            nullptr,
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -7008,16 +7008,18 @@ public:
 
   bool CreateWebRenderCommands(
     mozilla::wr::DisplayListBuilder& aBuilder,
     mozilla::wr::IpcResourceUpdateQueue& aResources,
     const StackingContextHelper& aSc,
     mozilla::layers::WebRenderLayerManager* aManager,
     nsDisplayListBuilder* aDisplayListBuilder) override;
 
+  bool CreateWebRenderCSSFilters(nsTArray<mozilla::wr::WrFilterOp>& wrFilters);
+
 private:
   // relative to mFrame
   nsRect mEffectsBounds;
 };
 
 /* A display item that applies a transformation to all of its descendant
  * elements.  This wrapper should only be used if there is a transform applied
  * to the root element.
--- a/layout/svg/nsFilterInstance.cpp
+++ b/layout/svg/nsFilterInstance.cpp
@@ -21,16 +21,17 @@
 #include "nsSVGDisplayableFrame.h"
 #include "nsCSSFilterInstance.h"
 #include "nsSVGFilterInstance.h"
 #include "nsSVGFilterPaintCallback.h"
 #include "nsSVGUtils.h"
 #include "SVGContentUtils.h"
 #include "FilterSupport.h"
 #include "gfx2DGlue.h"
+#include "mozilla/Unused.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::gfx;
 using namespace mozilla::image;
 
 FilterDescription
 nsFilterInstance::GetFilterDescription(nsIContent* aFilteredElement,
@@ -97,16 +98,183 @@ nsFilterInstance::PaintFilteredFrame(nsI
                             *metrics, filterChain, /* InputIsTainted */ true,
                             aPaintCallback, scaleMatrixInDevUnits,
                             aDirtyArea, nullptr, nullptr, nullptr);
   if (instance.IsInitialized()) {
     instance.Render(aCtx, aImgParams, aOpacity);
   }
 }
 
+bool
+nsFilterInstance::BuildWebRenderFilters(nsIFrame* aFilteredFrame,
+                                        const LayoutDeviceIntRect& aPreFilterBounds,
+                                        nsTArray<wr::WrFilterOp>& aWrFilters,
+                                        LayoutDeviceIntRect& aPostFilterBounds)
+{
+  aWrFilters.Clear();
+
+  auto& filterChain = aFilteredFrame->StyleEffects()->mFilters;
+  UniquePtr<UserSpaceMetrics> metrics = UserSpaceMetricsForFrame(aFilteredFrame);
+
+  // TODO: simply using an identity matrix here, was pulling the scale from a
+  // gfx context for the non-wr path.
+  gfxMatrix scaleMatrix;
+  gfxMatrix scaleMatrixInDevUnits =
+    scaleMatrix * nsSVGUtils::GetCSSPxToDevPxMatrix(aFilteredFrame);
+
+  // Hardcode inputIsTainted to true because we don't want JS to be able to
+  // read the rendered contents of aFilteredFrame.
+  bool inputIsTainted = true;
+  nsFilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(),
+                            *metrics, filterChain, inputIsTainted,
+                            nullptr, scaleMatrixInDevUnits,
+                            nullptr, nullptr, nullptr, nullptr);
+
+  if (!instance.IsInitialized()) {
+    return false;
+  }
+
+  Maybe<LayoutDeviceIntRect> finalClip;
+  bool srgb = true;
+  // We currently apply the clip on the stacking context after applying filters,
+  // but primitive subregions imply clipping after each filter and not just the
+  // end of the chain. For some types of filter it doesn't matter, but for those
+  // which sample outside of the location of the destination pixel like blurs,
+  // only clipping after could produce incorrect results, so we bail out in this
+  // case.
+  // We can lift this restriction once we have added support for primitive
+  // subregions to WebRender's filters.
+
+  // During the loop this tracks whether any of the previous filters in the chain
+  // affected by the primitive subregion.
+  bool chainIsAffectedByPrimSubregion = false;
+  // During the loop this tracks whether the current filter is affected by the
+  // primitive subregion.
+  bool filterIsAffectedByPrimSubregion = false;
+
+  for (const auto& primitive : instance.mFilterDescription.mPrimitives) {
+    bool primIsSrgb = primitive.OutputColorSpace() == gfx::ColorSpace::SRGB;
+    if (srgb && !primIsSrgb) {
+      wr::WrFilterOp filterOp = { wr::WrFilterOpType::SrgbToLinear };
+      aWrFilters.AppendElement(filterOp);
+      srgb = false;
+    } else if (!srgb && primIsSrgb) {
+      wr::WrFilterOp filterOp = { wr::WrFilterOpType::LinearToSrgb };
+      aWrFilters.AppendElement(filterOp);
+      srgb = true;
+    }
+
+    const PrimitiveAttributes& attr = primitive.Attributes();
+    auto subregion = LayoutDeviceIntRect::FromUnknownRect(
+      primitive.PrimitiveSubregion() + aPreFilterBounds.TopLeft().ToUnknownPoint()
+    );
+
+    if (!subregion.Contains(aPreFilterBounds)) {
+      if (!aPostFilterBounds.Contains(subregion)) {
+        filterIsAffectedByPrimSubregion = true;
+      }
+
+      subregion = subregion.Intersect(aPostFilterBounds);
+
+      if (finalClip.isNothing()) {
+        finalClip = Some(subregion);
+      } else if (!subregion.IsEqualEdges(finalClip.value())) {
+        // We don't currently support rendering a chain of filters with different
+        // primitive subregions in WebRender so bail out in that situation.
+        return false;
+      }
+    }
+
+    bool filterIsNoop = false;
+
+    if (attr.is<OpacityAttributes>()) {
+      float opacity = attr.as<OpacityAttributes>().mOpacity;
+      wr::WrFilterOp filterOp = { wr::WrFilterOpType::Opacity, opacity };
+      aWrFilters.AppendElement(filterOp);
+    } else if (attr.is<GaussianBlurAttributes>()) {
+      if (chainIsAffectedByPrimSubregion) {
+        // There's a clip that needs to apply before the blur filter, but
+        // WebRender only lets us apply the clip at the end of the filter
+        // chain. Clipping after a blur is not equivalent to clipping before
+        // a blur, so bail out.
+        return false;
+      }
+
+      const GaussianBlurAttributes& blur = attr.as<GaussianBlurAttributes>();
+
+      const Size& stdDev = blur.mStdDeviation;
+      if (stdDev.width != stdDev.height) {
+        return false;
+      }
+
+      float radius = stdDev.width;
+      if (radius != 0.0) {
+        wr::WrFilterOp filterOp = { wr::WrFilterOpType::Blur, radius };
+        aWrFilters.AppendElement(filterOp);
+      } else {
+        filterIsNoop = true;
+      }
+    } else if (attr.is<DropShadowAttributes>()) {
+      if (chainIsAffectedByPrimSubregion) {
+        // We have to bail out for the same reason we would with a blur filter.
+        return false;
+      }
+
+      const DropShadowAttributes& shadow = attr.as<DropShadowAttributes>();
+
+      const Size& stdDev = shadow.mStdDeviation;
+      if (stdDev.width != stdDev.height) {
+        return false;
+      }
+
+      float radius = stdDev.width;
+      wr::WrFilterOp filterOp = {
+        wr::WrFilterOpType::DropShadow,
+        radius,
+        {(float)shadow.mOffset.x, (float)shadow.mOffset.y},
+        wr::ToColorF(shadow.mColor)
+      };
+
+      aWrFilters.AppendElement(filterOp);
+    } else {
+      return false;
+    }
+
+    if (filterIsNoop &&
+        aWrFilters.Length() > 0 &&
+        (aWrFilters.LastElement().filter_type == wr::WrFilterOpType::SrgbToLinear ||
+         aWrFilters.LastElement().filter_type == wr::WrFilterOpType::LinearToSrgb)) {
+      // We pushed a color space conversion filter in prevision of applying
+      // another filter which turned out to be a no-op, so the conversion is
+      // unnecessary. Remove it from the filter list.
+      // This is both an optimization and a way to pass the wptest
+      // css/filter-effects/filter-scale-001.html for which the needless
+      // sRGB->linear->no-op->sRGB roundtrip introduces a slight error and we
+      // cannot add fuzziness to the test.
+      Unused << aWrFilters.PopLastElement();
+      srgb = !srgb;
+    }
+
+    chainIsAffectedByPrimSubregion |= filterIsAffectedByPrimSubregion;
+  }
+
+  if (!srgb) {
+    wr::WrFilterOp filterOp = { wr::WrFilterOpType::LinearToSrgb };
+    aWrFilters.AppendElement(filterOp);
+  }
+
+  // Only adjust the post filter clip if we are able to render this without
+  // fallback.
+  if (finalClip.isSome()) {
+    aPostFilterBounds = finalClip.value();
+  }
+
+  return true;
+}
+
 nsRegion
 nsFilterInstance::GetPostFilterDirtyArea(nsIFrame *aFilteredFrame,
                                          const nsRegion& aPreFilterDirtyRegion)
 {
   if (aPreFilterDirtyRegion.IsEmpty()) {
     return nsRegion();
   }
 
--- a/layout/svg/nsFilterInstance.h
+++ b/layout/svg/nsFilterInstance.h
@@ -16,16 +16,17 @@
 #include "nsRect.h"
 #include "nsSize.h"
 #include "nsSVGFilters.h"
 #include "nsSVGNumber2.h"
 #include "nsSVGNumberPair.h"
 #include "nsTArray.h"
 #include "nsIFrame.h"
 #include "mozilla/gfx/2D.h"
+#include "mozilla/webrender/WebRenderTypes.h"
 
 class gfxContext;
 class nsIFrame;
 class nsSVGFilterPaintCallback;
 
 namespace mozilla {
 namespace dom {
 class UserSpaceMetrics;
@@ -116,16 +117,25 @@ public:
    *   as aFilteredFrame's bbox ('bbox' is a specific SVG term), if non-null.
    * @param aPreFilterBounds The pre-filter visual overflow rect of
    *   aFilteredFrame, if non-null.
    */
   static nsRect GetPostFilterBounds(nsIFrame *aFilteredFrame,
                                     const gfxRect *aOverrideBBox = nullptr,
                                     const nsRect *aPreFilterBounds = nullptr);
 
+  /**
+   * Try to build WebRender filters for a frame if the filters applied to it are supported.
+   */
+  static bool
+  BuildWebRenderFilters(nsIFrame* aFilteredFrame,
+                        const mozilla::LayoutDeviceIntRect& aPreFilterBounds,
+                        nsTArray<mozilla::wr::WrFilterOp>& aWrFilters,
+                        mozilla::LayoutDeviceIntRect& aPostFilterBounds);
+
 private:
   /**
    * @param aTargetFrame The frame of the filtered element under consideration,
    *   may be null.
    * @param aTargetContent The filtered element itself.
    * @param aMetrics The metrics to resolve SVG lengths against.
    * @param aFilterChain The list of filters to apply.
    * @param aFilterInputIsTainted Describes whether the SourceImage / SourceAlpha
--- a/layout/svg/nsSVGIntegrationUtils.cpp
+++ b/layout/svg/nsSVGIntegrationUtils.cpp
@@ -1146,16 +1146,28 @@ nsSVGIntegrationUtils::PaintFilter(const
   RegularFramePaintCallback callback(aParams.builder, aParams.layerManager,
                                      offsets.offsetToUserSpaceInDevPx);
   nsRegion dirtyRegion = aParams.dirtyRect - offsets.offsetToBoundingBox;
 
   nsFilterInstance::PaintFilteredFrame(frame, &context, &callback,
                                        &dirtyRegion, aParams.imgParams, opacity);
 }
 
+bool
+nsSVGIntegrationUtils::BuildWebRenderFilters(nsIFrame *aFilteredFrame,
+                                             const mozilla::LayoutDeviceIntRect& aPreFilterBounds,
+                                             nsTArray<mozilla::wr::WrFilterOp>& aWrFilters,
+                                             mozilla::LayoutDeviceIntRect& aPostFilterBounds)
+{
+  return nsFilterInstance::BuildWebRenderFilters(aFilteredFrame,
+                                                 aPreFilterBounds,
+                                                 aWrFilters,
+                                                 aPostFilterBounds);
+}
+
 class PaintFrameCallback : public gfxDrawingCallback {
 public:
   PaintFrameCallback(nsIFrame* aFrame,
                      const nsSize aPaintServerSize,
                      const IntSize aRenderSize,
                      uint32_t aFlags)
    : mFrame(aFrame)
    , mPaintServerSize(aPaintServerSize)
--- a/layout/svg/nsSVGIntegrationUtils.h
+++ b/layout/svg/nsSVGIntegrationUtils.h
@@ -7,16 +7,17 @@
 #ifndef NSSVGINTEGRATIONUTILS_H_
 #define NSSVGINTEGRATIONUTILS_H_
 
 #include "ImgDrawResult.h"
 #include "gfxMatrix.h"
 #include "gfxRect.h"
 #include "nsRegionFwd.h"
 #include "mozilla/gfx/Rect.h"
+#include "mozilla/webrender/WebRenderTypes.h"
 
 class gfxContext;
 class gfxDrawable;
 class nsDisplayList;
 class nsDisplayListBuilder;
 class nsIFrame;
 
 struct nsRect;
@@ -194,16 +195,25 @@ public:
 
   /**
    * Paint non-SVG frame with filter and opacity effect.
    */
   static void
   PaintFilter(const PaintFramesParams& aParams);
 
   /**
+   * Try to build WebRender filters for a frame if the filters applied to it are supported.
+   */
+  static bool
+  BuildWebRenderFilters(nsIFrame *aFilteredFrame,
+                        const mozilla::LayoutDeviceIntRect& aPreFilterBounds,
+                        nsTArray<mozilla::wr::WrFilterOp>& aWrFilters,
+                        mozilla::LayoutDeviceIntRect& aPostFilterBounds);
+
+  /**
    * @param aRenderingContext the target rendering context in which the paint
    * server will be rendered
    * @param aTarget the target frame onto which the paint server will be
    * rendered
    * @param aPaintServer a first-continuation frame to use as the source
    * @param aFilter a filter to be applied when scaling
    * @param aDest the area the paint server image should be mapped to
    * @param aFill the area to be filled with copies of the paint server image