Merge inbound to mozilla-central. a=merge
authorCsoregi Natalia <ncsoregi@mozilla.com>
Mon, 13 May 2019 00:42:10 +0300
changeset 532369 b83d8a064f1694627e66f2dd3a683b66c350b3b3
parent 532365 4b3945b758896f5153e61a1b84a2b3614a98021a (current diff)
parent 532368 bcbaf50dca2841ec7a86f208661a5c27c49fd5fb (diff)
child 532373 e0910edd4fc73cc0ed2932e7c7e2744ae7972e1f
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
b83d8a064f16 / 68.0a1 / 20190512214232 / files
nightly linux64
b83d8a064f16 / 68.0a1 / 20190512214232 / files
nightly mac
b83d8a064f16 / 68.0a1 / 20190512214232 / files
nightly win32
b83d8a064f16 / 68.0a1 / 20190512214232 / files
nightly win64
b83d8a064f16 / 68.0a1 / 20190512214232 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
--- a/gfx/ipc/GfxMessageUtils.h
+++ b/gfx/ipc/GfxMessageUtils.h
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 __GFXMESSAGEUTILS_H__
 #define __GFXMESSAGEUTILS_H__
 
+#include "mozilla/webrender/webrender_ffi.h"
 #include "FilterSupport.h"
 #include "ImageTypes.h"
 #include "RegionBuilder.h"
 #include "base/process_util.h"
 #include "chrome/common/ipc_message_utils.h"
 #include "gfxFeature.h"
 #include "gfxFallback.h"
 #include "gfxPoint.h"
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -1141,26 +1141,28 @@ void DisplayListBuilder::PushLine(const 
                   &mCurrentSpaceAndClipChain, &aLine.bounds,
                   aLine.wavyLineThickness, aLine.orientation, &aLine.color,
                   aLine.style);
 }
 
 void DisplayListBuilder::PushShadow(const wr::LayoutRect& aRect,
                                     const wr::LayoutRect& aClip,
                                     bool aIsBackfaceVisible,
-                                    const wr::Shadow& aShadow) {
+                                    const wr::Shadow& aShadow,
+                                    bool aShouldInflate) {
   // Local clip_rects are translated inside of shadows, as they are assumed to
   // be part of the element drawing itself, and not a parent frame clipping it.
   // As such, it is not sound to apply the MergeClipLeaf optimization inside of
   // shadows. So we disable the optimization when we encounter a shadow.
   // Shadows don't span frames, so we don't have to worry about MergeClipLeaf
   // being re-enabled mid-shadow. The optimization is restored in PopAllShadows.
   SuspendClipLeafMerging();
   wr_dp_push_shadow(mWrState, aRect, aClip, aIsBackfaceVisible,
-                    &mCurrentSpaceAndClipChain, aShadow);
+                    &mCurrentSpaceAndClipChain, aShadow,
+                    aShouldInflate);
 }
 
 void DisplayListBuilder::PopAllShadows() {
   wr_dp_pop_all_shadows(mWrState);
   ResumeClipLeafMerging();
 }
 
 void DisplayListBuilder::SuspendClipLeafMerging() {
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -523,17 +523,18 @@ class DisplayListBuilder final {
                 wr::FontInstanceKey aFontKey,
                 Range<const wr::GlyphInstance> aGlyphBuffer,
                 const wr::GlyphOptions* aGlyphOptions = nullptr);
 
   void PushLine(const wr::LayoutRect& aClip, bool aIsBackfaceVisible,
                 const wr::Line& aLine);
 
   void PushShadow(const wr::LayoutRect& aBounds, const wr::LayoutRect& aClip,
-                  bool aIsBackfaceVisible, const wr::Shadow& aShadow);
+                  bool aIsBackfaceVisible, const wr::Shadow& aShadow,
+                  bool aShouldInflate);
 
   void PopAllShadows();
 
   void PushBoxShadow(const wr::LayoutRect& aRect, const wr::LayoutRect& aClip,
                      bool aIsBackfaceVisible, const wr::LayoutRect& aBoxBounds,
                      const wr::LayoutVector2D& aOffset,
                      const wr::ColorF& aColor, const float& aBlurRadius,
                      const float& aSpreadRadius,
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -2657,22 +2657,24 @@ pub extern "C" fn wr_dp_push_text(state:
 }
 
 #[no_mangle]
 pub extern "C" fn wr_dp_push_shadow(state: &mut WrState,
                                     _bounds: LayoutRect,
                                     _clip: LayoutRect,
                                     _is_backface_visible: bool,
                                     parent: &WrSpaceAndClipChain,
-                                    shadow: Shadow) {
+                                    shadow: Shadow,
+                                    should_inflate: bool) {
     debug_assert!(unsafe { is_in_main_thread() });
 
     state.frame_builder.dl_builder.push_shadow(
         &parent.to_webrender(state.pipeline_id),
         shadow.into(),
+        should_inflate,
     );
 }
 
 #[no_mangle]
 pub extern "C" fn wr_dp_pop_all_shadows(state: &mut WrState) {
     debug_assert!(unsafe { is_in_main_thread() });
 
     state.frame_builder.dl_builder.pop_all_shadows();
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -1,36 +1,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/. */
 
-use api::{AlphaType, ClipMode, ExternalImageType, FilterOp, ImageRendering};
+use api::{AlphaType, ClipMode, ExternalImageType, ImageRendering};
 use api::{YuvColorSpace, YuvFormat, ColorDepth, PremultipliedColorF, RasterSpace};
 use api::units::*;
 use crate::clip::{ClipDataStore, ClipNodeFlags, ClipNodeRange, ClipItem, ClipStore, ClipNodeInstance};
 use crate::clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex, CoordinateSystemId};
 use crate::glyph_rasterizer::GlyphFormat;
 use crate::gpu_cache::{GpuBlockData, GpuCache, GpuCacheHandle, GpuCacheAddress};
 use crate::gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders, ZBufferId, ZBufferIdGenerator};
 use crate::gpu_types::{ClipMaskInstance, SplitCompositeInstance, SnapOffsets};
 use crate::gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use crate::gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
-use crate::internal_types::{FastHashMap, SavedTargetIndex, TextureSource};
+use crate::internal_types::{FastHashMap, SavedTargetIndex, TextureSource, Filter};
 use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
 use crate::prim_store::{DeferredResolve, EdgeAaSegmentMask, PrimitiveInstanceKind, PrimitiveVisibilityIndex};
 use crate::prim_store::{VisibleGradientTile, PrimitiveInstance, PrimitiveOpacity, SegmentInstanceIndex};
 use crate::prim_store::{BrushSegment, ClipMaskKind, ClipTaskIndex, VECS_PER_SEGMENT};
 use crate::prim_store::{recompute_snap_offsets};
 use crate::prim_store::image::ImageSource;
 use crate::render_backend::DataStores;
 use crate::render_task::{RenderTaskAddress, RenderTaskId, RenderTaskTree, TileBlit};
 use crate::renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use crate::renderer::{BLOCKS_PER_UV_RECT, MAX_VERTEX_TEXTURE_WIDTH};
 use crate::resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache, ImageProperties};
-use crate::scene::FilterOpHelpers;
 use smallvec::SmallVec;
 use std::{f32, i32, usize};
 use crate::tiling::{RenderTargetContext};
 use crate::util::{project_rect, TransformedRectKind};
 
 // Special sentinel value recognized by the shader. It is considered to be
 // a dummy task that doesn't mask out anything.
 const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
@@ -631,16 +630,17 @@ impl BatchBuilder {
             );
 
         // TODO(gw): Calculating this for every primitive is a bit
         //           wasteful. We should probably cache this in
         //           the scroll node...
         let transform_kind = transform_id.transform_kind();
         let prim_info = &ctx.scratch.prim_info[prim_instance.visibility_info.0 as usize];
         let bounding_rect = &prim_info.clip_chain.pic_clip_rect;
+
         let z_id = z_generator.next();
 
         let prim_common_data = &ctx.data_stores.as_common_data(&prim_instance);
         let prim_rect = LayoutRect::new(
             prim_instance.prim_origin,
             prim_common_data.prim_size,
         );
 
@@ -1282,17 +1282,17 @@ impl BatchBuilder {
                                             Vec::new(),
                                         );
                                     }
                                 }
                             }
                             PictureCompositeMode::Filter(ref filter) => {
                                 assert!(filter.is_visible());
                                 match filter {
-                                    FilterOp::Blur(..) => {
+                                    Filter::Blur(..) => {
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
                                         );
                                         let (uv_rect_address, textures) = render_tasks.resolve_surface(
                                             surface.expect("bug: surface must be allocated by now"),
                                             gpu_cache,
                                         );
                                         let key = BatchKey::new(
@@ -1318,17 +1318,115 @@ impl BatchBuilder {
 
                                         batcher.current_batch_list().push_single_instance(
                                             key,
                                             bounding_rect,
                                             z_id,
                                             PrimitiveInstanceData::from(instance),
                                         );
                                     }
-                                    FilterOp::DropShadow(offset, ..) => {
+                                    Filter::DropShadowStack(shadows) => {
+                                        // Draw an instance per shadow first, following by the content.
+
+                                        // The shadows and the content get drawn as a brush image.
+                                        let kind = BatchKind::Brush(
+                                            BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
+                                        );
+
+                                        // Gets the saved render task ID of the content, which is
+                                        // deeper in the render task tree than the direct child.
+                                        let secondary_id = picture.secondary_render_task_id.expect("no secondary!?");
+                                        let saved_index = render_tasks[secondary_id].saved_index.expect("no saved index!?");
+                                        debug_assert_ne!(saved_index, SavedTargetIndex::PENDING);
+
+                                        // Build BatchTextures for shadow/content
+                                        let shadow_textures = BatchTextures::render_target_cache();
+                                        let content_textures = BatchTextures {
+                                            colors: [
+                                                TextureSource::RenderTaskCache(saved_index),
+                                                TextureSource::Invalid,
+                                                TextureSource::Invalid,
+                                            ],
+                                        };
+
+                                        // Build batch keys for shadow/content
+                                        let shadow_key = BatchKey::new(kind, non_segmented_blend_mode, shadow_textures);
+                                        let content_key = BatchKey::new(kind, non_segmented_blend_mode, content_textures);
+
+                                        // Retrieve the UV rect addresses for shadow/content.
+                                        let cache_task_id = surface
+                                            .expect("bug: surface must be allocated by now");
+                                        let shadow_uv_rect_address = render_tasks[cache_task_id]
+                                            .get_texture_address(gpu_cache)
+                                            .as_int();
+                                        let content_uv_rect_address = render_tasks[secondary_id]
+                                            .get_texture_address(gpu_cache)
+                                            .as_int();
+
+                                        for (shadow, shadow_gpu_data) in shadows.iter().zip(picture.extra_gpu_data_handles.iter()) {
+                                            // Get the GPU cache address of the extra data handle.
+                                            let shadow_prim_address = gpu_cache.get_address(shadow_gpu_data);
+
+                                            let shadow_rect = prim_header.local_rect.translate(&shadow.offset);
+
+                                            let shadow_prim_header = PrimitiveHeader {
+                                                local_rect: shadow_rect,
+                                                specific_prim_address: shadow_prim_address,
+                                                ..prim_header
+                                            };
+
+                                            let shadow_prim_header_index = prim_headers.push(&shadow_prim_header, z_id, [
+                                                ShaderColorMode::Alpha as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
+                                                RasterizationSpace::Screen as i32,
+                                                get_shader_opacity(1.0),
+                                                0,
+                                            ]);
+
+                                            let shadow_instance = BrushInstance {
+                                                prim_header_index: shadow_prim_header_index,
+                                                clip_task_address,
+                                                segment_index: INVALID_SEGMENT_INDEX,
+                                                edge_flags: EdgeAaSegmentMask::empty(),
+                                                brush_flags,
+                                                user_data: shadow_uv_rect_address,
+                                            };
+
+                                            batcher.current_batch_list().push_single_instance(
+                                                shadow_key,
+                                                bounding_rect,
+                                                z_id,
+                                                PrimitiveInstanceData::from(shadow_instance),
+                                            );
+                                        }
+                                        let z_id_content = z_generator.next();
+
+                                        let content_prim_header_index = prim_headers.push(&prim_header, z_id_content, [
+                                            ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
+                                            RasterizationSpace::Screen as i32,
+                                            get_shader_opacity(1.0),
+                                            0,
+                                        ]);
+
+                                        let content_instance = BrushInstance {
+                                            prim_header_index: content_prim_header_index,
+                                            clip_task_address,
+                                            segment_index: INVALID_SEGMENT_INDEX,
+                                            edge_flags: EdgeAaSegmentMask::empty(),
+                                            brush_flags,
+                                            user_data: content_uv_rect_address,
+                                        };
+
+                                        batcher.current_batch_list().push_single_instance(
+                                            content_key,
+                                            bounding_rect,
+                                            z_id_content,
+                                            PrimitiveInstanceData::from(content_instance),
+                                        );
+                                    }
+                                    Filter::DropShadow(shadow) => {
                                         // Draw an instance of the shadow first, following by the content.
 
                                         // Both the shadow and the content get drawn as a brush image.
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
                                         );
 
                                         // Gets the saved render task ID of the content, which is
@@ -1356,29 +1454,29 @@ impl BatchBuilder {
                                         let shadow_uv_rect_address = render_tasks[cache_task_id]
                                             .get_texture_address(gpu_cache)
                                             .as_int();
                                         let content_uv_rect_address = render_tasks[secondary_id]
                                             .get_texture_address(gpu_cache)
                                             .as_int();
 
                                         // Get the GPU cache address of the extra data handle.
-                                        let shadow_prim_address = gpu_cache.get_address(&picture.extra_gpu_data_handle);
+                                        let shadow_prim_address = gpu_cache.get_address(&picture.extra_gpu_data_handles[0]);
 
                                         let z_id_shadow = z_id;
                                         let z_id_content = z_generator.next();
 
                                         let content_prim_header_index = prim_headers.push(&prim_header, z_id_content, [
                                             ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
                                             RasterizationSpace::Screen as i32,
                                             get_shader_opacity(1.0),
                                             0,
                                         ]);
 
-                                        let shadow_rect = prim_header.local_rect.translate(&offset);
+                                        let shadow_rect = prim_header.local_rect.translate(&shadow.offset);
 
                                         let shadow_prim_header = PrimitiveHeader {
                                             local_rect: shadow_rect,
                                             snap_offsets: prim_info.shadow_snap_offsets,
                                             specific_prim_address: shadow_prim_address,
                                             ..prim_header
                                         };
 
@@ -1418,57 +1516,59 @@ impl BatchBuilder {
                                             content_key,
                                             bounding_rect,
                                             z_id_content,
                                             PrimitiveInstanceData::from(content_instance),
                                         );
                                     }
                                     _ => {
                                         let filter_mode = match filter {
-                                            FilterOp::Identity => 1, // matches `Contrast(1)`
-                                            FilterOp::Blur(..) => 0,
-                                            FilterOp::Contrast(..) => 1,
-                                            FilterOp::Grayscale(..) => 2,
-                                            FilterOp::HueRotate(..) => 3,
-                                            FilterOp::Invert(..) => 4,
-                                            FilterOp::Saturate(..) => 5,
-                                            FilterOp::Sepia(..) => 6,
-                                            FilterOp::Brightness(..) => 7,
-                                            FilterOp::Opacity(..) => 8,
-                                            FilterOp::DropShadow(..) => 9,
-                                            FilterOp::ColorMatrix(..) => 10,
-                                            FilterOp::SrgbToLinear => 11,
-                                            FilterOp::LinearToSrgb => 12,
-                                            FilterOp::ComponentTransfer => unreachable!(),
+                                            Filter::Identity => 1, // matches `Contrast(1)`
+                                            Filter::Blur(..) => 0,
+                                            Filter::Contrast(..) => 1,
+                                            Filter::Grayscale(..) => 2,
+                                            Filter::HueRotate(..) => 3,
+                                            Filter::Invert(..) => 4,
+                                            Filter::Saturate(..) => 5,
+                                            Filter::Sepia(..) => 6,
+                                            Filter::Brightness(..) => 7,
+                                            Filter::Opacity(..) => 8,
+                                            Filter::DropShadow(..) |
+                                            Filter::DropShadowStack(..) => 9,
+                                            Filter::ColorMatrix(..) => 10,
+                                            Filter::SrgbToLinear => 11,
+                                            Filter::LinearToSrgb => 12,
+                                            Filter::ComponentTransfer => unreachable!(),
                                         };
 
                                         let user_data = match filter {
-                                            FilterOp::Identity => 0x10000i32, // matches `Contrast(1)`
-                                            FilterOp::Contrast(amount) |
-                                            FilterOp::Grayscale(amount) |
-                                            FilterOp::Invert(amount) |
-                                            FilterOp::Saturate(amount) |
-                                            FilterOp::Sepia(amount) |
-                                            FilterOp::Brightness(amount) |
-                                            FilterOp::Opacity(_, amount) => {
+                                            Filter::Identity => 0x10000i32, // matches `Contrast(1)`
+                                            Filter::Contrast(amount) |
+                                            Filter::Grayscale(amount) |
+                                            Filter::Invert(amount) |
+                                            Filter::Saturate(amount) |
+                                            Filter::Sepia(amount) |
+                                            Filter::Brightness(amount) |
+                                            Filter::Opacity(_, amount) => {
                                                 (amount * 65536.0) as i32
                                             }
-                                            FilterOp::SrgbToLinear | FilterOp::LinearToSrgb => 0,
-                                            FilterOp::HueRotate(angle) => {
+                                            Filter::SrgbToLinear | Filter::LinearToSrgb => 0,
+                                            Filter::HueRotate(angle) => {
                                                 (0.01745329251 * angle * 65536.0) as i32
                                             }
                                             // Go through different paths
-                                            FilterOp::Blur(..) |
-                                            FilterOp::DropShadow(..) => {
+                                            Filter::Blur(..) |
+                                            Filter::DropShadowStack(..) |
+                                            Filter::DropShadow(..) => {
                                                 unreachable!();
                                             }
-                                            FilterOp::ColorMatrix(_) => {
-                                                picture.extra_gpu_data_handle.as_int(gpu_cache)
+                                            Filter::ColorMatrix(_) => {
+                                                picture.extra_gpu_data_handles[0].as_int(gpu_cache)
                                             }
-                                            FilterOp::ComponentTransfer => unreachable!(),
+                                            Filter::ComponentTransfer => unreachable!(),
                                         };
 
                                         let (uv_rect_address, textures) = render_tasks.resolve_surface(
                                             surface.expect("bug: surface must be allocated by now"),
                                             gpu_cache,
                                         );
 
                                         let key = BatchKey::new(
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -14,17 +14,17 @@ use api::{ClipMode, PrimitiveKeyKind, Tr
 use api::units::*;
 use crate::clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use crate::frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use crate::glyph_rasterizer::FontInstance;
 use crate::hit_test::{HitTestingItem, HitTestingScene};
 use crate::image::simplify_repeated_primitive;
 use crate::intern::Interner;
-use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo};
+use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo, Filter};
 use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
 use crate::picture::{BlitReason, PrimitiveList, TileCache};
 use crate::prim_store::{PrimitiveInstance, PrimitiveSceneData};
 use crate::prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
 use crate::prim_store::{ScrollNodeAndClipChain, PictureIndex};
 use crate::prim_store::{InternablePrimitive, SegmentInstanceIndex};
 use crate::prim_store::{register_prim_chase_id, get_line_decoration_sizes};
 use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
@@ -1275,17 +1275,17 @@ impl<'a> DisplayListFlattener<'a> {
             }
             DisplayItem::PushShadow(info) => {
                 let clip_and_scroll = self.get_clip_and_scroll(
                     &info.space_and_clip.clip_id,
                     &info.space_and_clip.spatial_id,
                     apply_pipeline_clip
                 );
 
-                self.push_shadow(info.shadow, clip_and_scroll);
+                self.push_shadow(info.shadow, clip_and_scroll, info.should_inflate);
             }
             DisplayItem::PopAllShadows => {
                 self.pop_all_shadows();
             }
         }
 
         None
     }
@@ -1751,21 +1751,21 @@ impl<'a> DisplayListFlattener<'a> {
                 ClipChainId::NONE,
                 stacking_context.spatial_node_index,
                 &mut self.interners,
             );
         }
 
         // For each filter, create a new image with that composite mode.
         let mut current_filter_data_index = 0;
-        for filter in &stacking_context.composite_ops.filters {
-            let filter = filter.sanitize();
+        for filter in &mut stacking_context.composite_ops.filters {
+            filter.sanitize();
 
-            let composite_mode = Some(match filter {
-                FilterOp::ComponentTransfer => {
+            let composite_mode = Some(match *filter {
+                Filter::ComponentTransfer => {
                     let filter_data =
                         &stacking_context.composite_ops.filter_datas[current_filter_data_index];
                     let filter_data = filter_data.sanitize();
                     current_filter_data_index = current_filter_data_index + 1;
                     if filter_data.is_identity() {
                         continue
                     } else {
                         let filter_data_key = SFilterDataKey {
@@ -2093,22 +2093,24 @@ impl<'a> DisplayListFlattener<'a> {
         self.id_to_index_mapper.map_spatial_node(new_node_id, node_index);
         node_index
     }
 
     pub fn push_shadow(
         &mut self,
         shadow: Shadow,
         clip_and_scroll: ScrollNodeAndClipChain,
+        should_inflate: bool,
     ) {
         // Store this shadow in the pending list, for processing
         // during pop_all_shadows.
         self.pending_shadow_items.push_back(ShadowItem::Shadow(PendingShadow {
             shadow,
             clip_and_scroll,
+            should_inflate,
         }));
     }
 
     pub fn pop_all_shadows(
         &mut self,
     ) {
         assert!(!self.pending_shadow_items.is_empty(), "popped shadows, but none were present");
 
@@ -2197,25 +2199,26 @@ impl<'a> DisplayListFlattener<'a> {
 
                     // No point in adding a shadow here if there were no primitives
                     // added to the shadow.
                     if !prims.is_empty() {
                         // Create a picture that the shadow primitives will be added to. If the
                         // blur radius is 0, the code in Picture::prepare_for_render will
                         // detect this and mark the picture to be drawn directly into the
                         // parent picture, which avoids an intermediate surface and blur.
-                        let blur_filter = FilterOp::Blur(std_deviation).sanitize();
+                        let mut blur_filter = Filter::Blur(std_deviation);
+                        blur_filter.sanitize();
                         let composite_mode = PictureCompositeMode::Filter(blur_filter);
                         let composite_mode_key = Some(composite_mode.clone()).into();
                         let is_backface_visible = true; //TODO: double check this
 
                         // Pass through configuration information about whether WR should
                         // do the bounding rect inflation for text shadows.
                         let options = PictureOptions {
-                            inflate_if_required: pending_shadow.shadow.should_inflate,
+                            inflate_if_required: pending_shadow.should_inflate,
                         };
 
                         // Create the primitive to draw the shadow picture into the scene.
                         let shadow_pic_index = PictureIndex(self.prim_store.pictures
                             .alloc()
                             .init(PicturePrimitive::new_image(
                                 Some(composite_mode),
                                 Picture3DContext::Out,
@@ -2307,26 +2310,26 @@ impl<'a> DisplayListFlattener<'a> {
     )
     where
         P: InternablePrimitive + CreateShadow,
         Interners: AsMut<Interner<P>>,
     {
         // Offset the local rect and clip rect by the shadow offset.
         let mut info = pending_primitive.info.clone();
         info.rect = info.rect.translate(&pending_shadow.shadow.offset);
-        info.clip_rect = info.clip_rect.translate(&pending_shadow.shadow.offset);
+        info.clip_rect = info.clip_rect.translate(
+            &pending_shadow.shadow.offset
+        );
 
         // Construct and add a primitive for the given shadow.
         let shadow_prim_instance = self.create_primitive(
             &info,
             pending_primitive.clip_and_scroll.clip_chain_id,
             pending_primitive.clip_and_scroll.spatial_node_index,
-            pending_primitive.prim.create_shadow(
-                &pending_shadow.shadow,
-            ),
+            pending_primitive.prim.create_shadow(&pending_shadow.shadow),
         );
 
         // Add the new primitive to the shadow picture.
         prims.push(shadow_prim_instance);
     }
 
     fn add_shadow_prim_to_draw_list<P>(
         &mut self,
@@ -3026,16 +3029,17 @@ pub struct PendingPrimitive<T> {
     info: LayoutPrimitiveInfo,
     prim: T,
 }
 
 /// As shadows are pushed, they are stored as pending
 /// shadows, and handled at once during pop_all_shadows.
 pub struct PendingShadow {
     shadow: Shadow,
+    should_inflate: bool,
     clip_and_scroll: ScrollNodeAndClipChain,
 }
 
 pub enum ShadowItem {
     Shadow(PendingShadow),
     Image(PendingPrimitive<Image>),
     LineDecoration(PendingPrimitive<LineDecoration>),
     NormalBorder(PendingPrimitive<NormalBorderPrim>),
--- a/gfx/wr/webrender/src/internal_types.rs
+++ b/gfx/wr/webrender/src/internal_types.rs
@@ -1,15 +1,16 @@
 /* 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/. */
 
 use api::{DebugCommand, DocumentId, ExternalImageData, ExternalImageId};
-use api::{ImageFormat, ItemTag, NotificationRequest};
+use api::{ImageFormat, ItemTag, NotificationRequest, Shadow, FilterOp, MAX_BLUR_RADIUS};
 use api::units::*;
+use api;
 use crate::device::TextureFilter;
 use crate::renderer::PipelineInfo;
 use crate::gpu_cache::GpuCacheUpdateList;
 use fxhash::FxHasher;
 use plane_split::BspSplitter;
 use crate::profiler::BackendProfileCounters;
 use std::{usize, i32};
 use std::collections::{HashMap, HashSet};
@@ -25,16 +26,146 @@ use crate::capture::PlainExternalImage;
 use crate::tiling;
 
 pub type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
 pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>;
 
 /// A concrete plane splitter type used in WebRender.
 pub type PlaneSplitter = BspSplitter<f64, WorldPixel>;
 
+/// An arbitrary number which we assume opacity is invisible below.
+const OPACITY_EPSILON: f32 = 0.001;
+
+/// Equivalent to api::FilterOp with added internal information
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Filter {
+    Identity,
+    Blur(f32),
+    Brightness(f32),
+    Contrast(f32),
+    Grayscale(f32),
+    HueRotate(f32),
+    Invert(f32),
+    Opacity(api::PropertyBinding<f32>, f32),
+    Saturate(f32),
+    Sepia(f32),
+    DropShadow(Shadow),
+    #[allow(dead_code)]
+    DropShadowStack(Vec<Shadow>),
+    ColorMatrix([f32; 20]),
+    SrgbToLinear,
+    LinearToSrgb,
+    ComponentTransfer,
+}
+
+impl Filter {
+    /// Ensure that the parameters for a filter operation
+    /// are sensible.
+    pub fn sanitize(&mut self) {
+        match self {
+            Filter::Blur(ref mut radius) => {
+                *radius = radius.min(MAX_BLUR_RADIUS);
+            }
+            Filter::DropShadow(ref mut shadow) => {
+                shadow.blur_radius = shadow.blur_radius.min(MAX_BLUR_RADIUS);
+            }
+            Filter::DropShadowStack(ref mut stack) => {
+                for shadow in stack {
+                    shadow.blur_radius = shadow.blur_radius.min(MAX_BLUR_RADIUS);
+                }
+            }
+            _ => {},
+        }
+    }
+
+    pub fn is_visible(&self) -> bool {
+        match *self {
+            Filter::Identity |
+            Filter::Blur(..) |
+            Filter::Brightness(..) |
+            Filter::Contrast(..) |
+            Filter::Grayscale(..) |
+            Filter::HueRotate(..) |
+            Filter::Invert(..) |
+            Filter::Saturate(..) |
+            Filter::Sepia(..) |
+            Filter::DropShadow(..) |
+            Filter::DropShadowStack(..) |
+            Filter::ColorMatrix(..) |
+            Filter::SrgbToLinear |
+            Filter::LinearToSrgb |
+            Filter::ComponentTransfer  => true,
+            Filter::Opacity(_, amount) => {
+                amount > OPACITY_EPSILON
+            }
+        }
+    }
+
+    pub fn is_noop(&self) -> bool {
+        match *self {
+            Filter::Identity => false, // this is intentional
+            Filter::Blur(length) => length == 0.0,
+            Filter::Brightness(amount) => amount == 1.0,
+            Filter::Contrast(amount) => amount == 1.0,
+            Filter::Grayscale(amount) => amount == 0.0,
+            Filter::HueRotate(amount) => amount == 0.0,
+            Filter::Invert(amount) => amount == 0.0,
+            Filter::Opacity(_, amount) => amount >= 1.0,
+            Filter::Saturate(amount) => amount == 1.0,
+            Filter::Sepia(amount) => amount == 0.0,
+            Filter::DropShadowStack(ref shadows) => {
+                for shadow in shadows {
+                    if shadow.offset.x != 0.0 || shadow.offset.y != 0.0 || shadow.blur_radius != 0.0 {
+                        return false;
+                    }
+                }
+
+                true
+            }
+            Filter::DropShadow(shadow) => {
+                shadow.offset.x == 0.0 && shadow.offset.y == 0.0 && shadow.blur_radius == 0.0
+            },
+            Filter::ColorMatrix(matrix) => {
+                matrix == [1.0, 0.0, 0.0, 0.0,
+                           0.0, 1.0, 0.0, 0.0,
+                           0.0, 0.0, 1.0, 0.0,
+                           0.0, 0.0, 0.0, 1.0,
+                           0.0, 0.0, 0.0, 0.0]
+            }
+            Filter::SrgbToLinear |
+            Filter::LinearToSrgb |
+            Filter::ComponentTransfer => false,
+        }
+    }
+}
+
+impl From<FilterOp> for Filter {
+    fn from(op: FilterOp) -> Self {
+        match op {
+            FilterOp::Identity => Filter::Identity,
+            FilterOp::Blur(r) => Filter::Blur(r),
+            FilterOp::Brightness(b) => Filter::Brightness(b),
+            FilterOp::Contrast(c) => Filter::Contrast(c),
+            FilterOp::Grayscale(g) => Filter::Grayscale(g),
+            FilterOp::HueRotate(h) => Filter::HueRotate(h),
+            FilterOp::Invert(i) => Filter::Invert(i),
+            FilterOp::Opacity(binding, opacity) => Filter::Opacity(binding, opacity),
+            FilterOp::Saturate(s) => Filter::Saturate(s),
+            FilterOp::Sepia(s) => Filter::Sepia(s),
+            FilterOp::DropShadow(shadow) => Filter::DropShadow(shadow),
+            FilterOp::ColorMatrix(mat) => Filter::ColorMatrix(mat),
+            FilterOp::SrgbToLinear => Filter::SrgbToLinear,
+            FilterOp::LinearToSrgb => Filter::LinearToSrgb,
+            FilterOp::ComponentTransfer => Filter::ComponentTransfer,
+        }
+    }
+}
+
 /// An ID for a texture that is owned by the `texture_cache` module.
 ///
 /// This can include atlases or standalone textures allocated via the texture
 /// cache (e.g.  if an image is too large to be added to an atlas). The texture
 /// cache manages the allocation and freeing of these IDs, and the rendering
 /// thread maintains a map from cache texture ID to native texture.
 ///
 /// We never reuse IDs, so we use a u64 here to be safe.
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,41 +1,41 @@
 /* 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/. */
 
-use api::{FilterOp, MixBlendMode, PipelineId, PremultipliedColorF};
+use api::{MixBlendMode, PipelineId, PremultipliedColorF};
 use api::{PropertyBinding, PropertyBindingId};
 use api::{DebugFlags, RasterSpace, ColorF, ImageKey, ClipMode};
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipChainId, ClipChainNode, ClipItem, ClipStore, ClipDataStore, ClipChainStack};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX,
     ClipScrollTree, CoordinateSystemId, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
 };
 use crate::debug_colors;
 use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D, Vector2D};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::intern::ItemUid;
-use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
+use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter};
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
 use crate::prim_store::SpaceMapper;
 use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
 use crate::print_tree::PrintTreePrinter;
 use crate::render_backend::DataStores;
 use crate::render_task::{ClearMode, RenderTask, TileBlit};
-use crate::render_task::{RenderTaskId, RenderTaskLocation};
+use crate::render_task::{RenderTaskId, RenderTaskLocation, BlurTaskCache};
 use crate::resource_cache::ResourceCache;
-use crate::scene::{FilterOpHelpers, SceneProperties};
+use crate::scene::SceneProperties;
 use crate::scene_builder::Interners;
 use smallvec::SmallVec;
 use std::{mem, u16};
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::texture_cache::TextureCacheHandle;
 use crate::tiling::RenderTargetKind;
 use crate::util::{ComparableVec, TransformedRectKind, MatrixHelpers, MaxRect, scale_factors};
 use crate::filterdata::{FilterDataHandle};
@@ -1128,17 +1128,17 @@ impl TileCache {
         // tile (in cases where the picture local rect affects the tile, but the clip
         // rect eventually means it doesn't affect that tile).
         // TODO(gw): Get picture clips earlier (during the initial picture traversal
         //           pass) so that we can calculate these correctly.
         let include_clip_rect = match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index,.. } => {
                 // Pictures can depend on animated opacity bindings.
                 let pic = &pictures[pic_index.0];
-                if let Some(PictureCompositeMode::Filter(FilterOp::Opacity(binding, _))) = pic.requested_composite_mode {
+                if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.requested_composite_mode {
                     opacity_bindings.push(binding.into());
                 }
 
                 false
             }
             PrimitiveInstanceKind::Rectangle { opacity_binding_index, .. } => {
                 if opacity_binding_index != OpacityBindingIndex::INVALID {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
@@ -1889,17 +1889,17 @@ bitflags! {
 /// onto the target it belongs to.
 #[allow(dead_code)]
 #[derive(Debug, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub enum PictureCompositeMode {
     /// Apply CSS mix-blend-mode effect.
     MixBlend(MixBlendMode),
     /// Apply a CSS filter (except component transfer).
-    Filter(FilterOp),
+    Filter(Filter),
     /// Apply a component transfer filter.
     ComponentTransferFilter(FilterDataHandle),
     /// Draw to intermediate surface, copy straight across. This
     /// is used for CSS isolation, and plane splitting.
     Blit(BlitReason),
     /// Used to cache a picture as a series of tiles.
     TileCache {
         clear_color: ColorF,
@@ -2183,20 +2183,20 @@ pub struct PicturePrimitive {
 
     pub raster_config: Option<RasterConfig>,
     pub context_3d: Picture3DContext<OrderedPictureChild>,
 
     // If requested as a frame output (for rendering
     // pages to a texture), this is the pipeline this
     // picture is the root of.
     pub frame_output_pipeline_id: Option<PipelineId>,
-    // An optional cache handle for storing extra data
+    // Optional cache handles for storing extra data
     // in the GPU cache, depending on the type of
     // picture.
-    pub extra_gpu_data_handle: GpuCacheHandle,
+    pub extra_gpu_data_handles: SmallVec<[GpuCacheHandle; 1]>,
 
     /// The spatial node index of this picture when it is
     /// composited into the parent picture.
     pub spatial_node_index: SpatialNodeIndex,
 
     /// The local rect of this picture. It is built
     /// dynamically when updating visibility. It takes
     /// into account snapping in device space for its
@@ -2263,17 +2263,17 @@ impl PicturePrimitive {
             }
         }
     }
 
     fn resolve_scene_properties(&mut self, properties: &SceneProperties) -> bool {
         match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(ref mut filter)) => {
                 match *filter {
-                    FilterOp::Opacity(ref binding, ref mut value) => {
+                    Filter::Opacity(ref binding, ref mut value) => {
                         *value = properties.resolve_float(binding);
                     }
                     _ => {}
                 }
 
                 filter.is_visible()
             }
             _ => true,
@@ -2331,17 +2331,17 @@ impl PicturePrimitive {
         PicturePrimitive {
             prim_list,
             state: None,
             secondary_render_task_id: None,
             requested_composite_mode,
             raster_config: None,
             context_3d,
             frame_output_pipeline_id,
-            extra_gpu_data_handle: GpuCacheHandle::new(),
+            extra_gpu_data_handles: SmallVec::new(),
             apply_local_clip_rect,
             is_backface_visible,
             pipeline_id,
             requested_raster_space,
             spatial_node_index,
             snapped_local_rect: LayoutRect::zero(),
             unsnapped_local_rect: LayoutRect::zero(),
             tile_cache,
@@ -2462,17 +2462,17 @@ impl PicturePrimitive {
                     }
                 };
                 let transform = map_pic_to_raster.get_transform();
 
                 let (root, port) = match raster_config.composite_mode {
                     PictureCompositeMode::TileCache { .. } => {
                         unreachable!();
                     }
-                    PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)) => {
+                    PictureCompositeMode::Filter(Filter::Blur(blur_radius)) => {
                         let blur_std_deviation = blur_radius * device_pixel_scale.0;
                         let scale_factors = scale_factors(&transform);
                         let blur_std_deviation = DeviceSize::new(
                             blur_std_deviation * scale_factors.0,
                             blur_std_deviation * scale_factors.1
                         );
                         let inflation_factor = frame_state.surfaces[raster_config.surface_index.0].inflation_factor;
                         let inflation_factor = (inflation_factor * device_pixel_scale.0).ceil() as i32;
@@ -2513,30 +2513,29 @@ impl PicturePrimitive {
                             Vec::new(),
                             uv_rect_kind,
                             raster_spatial_node_index,
                             device_pixel_scale,
                         );
 
                         let picture_task_id = frame_state.render_tasks.add(picture_task);
 
-                        let blur_render_task = RenderTask::new_blur(
+                        let blur_render_task_id = RenderTask::new_blur(
                             blur_std_deviation,
                             picture_task_id,
                             frame_state.render_tasks,
                             RenderTargetKind::Color,
                             ClearMode::Transparent,
+                            None,
                         );
 
-                        let render_task_id = frame_state.render_tasks.add(blur_render_task);
-
-                        (render_task_id, picture_task_id)
+                        (blur_render_task_id, picture_task_id)
                     }
-                    PictureCompositeMode::Filter(FilterOp::DropShadow(_, blur_radius, _)) => {
-                        let blur_std_deviation = blur_radius * device_pixel_scale.0;
+                    PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => {
+                        let blur_std_deviation = shadow.blur_radius * device_pixel_scale.0;
                         let blur_range = (blur_std_deviation * BLUR_SAMPLE_SCALE).ceil() as i32;
                         let rounded_std_dev = blur_std_deviation.round();
                         let rounded_std_dev = DeviceSize::new(rounded_std_dev, rounded_std_dev);
                         // The clipped field is the part of the picture that is visible
                         // on screen. The unclipped field is the screen-space rect of
                         // the complete picture, if no screen / clip-chain was applied
                         // (this includes the extra space for blur region). To ensure
                         // that we draw a large enough part of the picture to get correct
@@ -2568,29 +2567,90 @@ impl PicturePrimitive {
                             uv_rect_kind,
                             raster_spatial_node_index,
                             device_pixel_scale,
                         );
                         picture_task.mark_for_saving();
 
                         let picture_task_id = frame_state.render_tasks.add(picture_task);
 
-                        let blur_render_task = RenderTask::new_blur(
+                        let blur_render_task_id = RenderTask::new_blur(
                             rounded_std_dev,
                             picture_task_id,
                             frame_state.render_tasks,
                             RenderTargetKind::Color,
                             ClearMode::Transparent,
+                            None,
                         );
 
                         self.secondary_render_task_id = Some(picture_task_id);
 
-                        let render_task_id = frame_state.render_tasks.add(blur_render_task);
-
-                        (render_task_id, picture_task_id)
+                        (blur_render_task_id, picture_task_id)
+                    }
+                    PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                        let mut max_std_deviation = 0.0;
+                        for shadow in shadows {
+                            // TODO(nical) presumably we should compute the clipped rect for each shadow
+                            // and compute the union of them to determine what we need to rasterize and blur?
+                            max_std_deviation = f32::max(max_std_deviation, shadow.blur_radius * device_pixel_scale.0);
+                        }
+        
+                        max_std_deviation = max_std_deviation.round();
+                        let max_blur_range = (max_std_deviation * BLUR_SAMPLE_SCALE).ceil() as i32;
+                        let mut device_rect = clipped.inflate(max_blur_range, max_blur_range)
+                                .intersection(&unclipped.to_i32())
+                                .unwrap();
+                        device_rect.size = RenderTask::adjusted_blur_source_size(
+                            device_rect.size,
+                            DeviceSize::new(max_std_deviation, max_std_deviation),
+                        );
+        
+                        let uv_rect_kind = calculate_uv_rect_kind(
+                            &pic_rect,
+                            &transform,
+                            &device_rect,
+                            device_pixel_scale,
+                            true,
+                        );
+        
+                        let mut picture_task = RenderTask::new_picture(
+                            RenderTaskLocation::Dynamic(None, device_rect.size),
+                            unclipped.size,
+                            pic_index,
+                            device_rect.origin,
+                            Vec::new(),
+                            uv_rect_kind,
+                            raster_spatial_node_index,
+                            device_pixel_scale,
+                        );
+                        picture_task.mark_for_saving();
+        
+                        let picture_task_id = frame_state.render_tasks.add(picture_task);
+        
+                        self.secondary_render_task_id = Some(picture_task_id);
+        
+                        let mut blur_tasks = BlurTaskCache::default();
+        
+                        self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+        
+                        let mut blur_render_task_id = picture_task_id;
+                        for shadow in shadows {
+                            let std_dev = f32::round(shadow.blur_radius * device_pixel_scale.0);
+                            blur_render_task_id = RenderTask::new_blur(
+                                DeviceSize::new(std_dev, std_dev),
+                                picture_task_id,
+                                frame_state.render_tasks,
+                                RenderTargetKind::Color,
+                                ClearMode::Transparent,
+                                Some(&mut blur_tasks),
+                            );      
+                        }
+        
+                        // TODO(nical) the second one should to be the blur's task id but we have several blurs now
+                        (picture_task_id, blur_render_task_id)
                     }
                     PictureCompositeMode::MixBlend(..) if !frame_context.fb_config.gpu_supports_advanced_blend => {
                         let uv_rect_kind = calculate_uv_rect_kind(
                             &pic_rect,
                             &transform,
                             &clipped,
                             device_pixel_scale,
                             true,
@@ -2997,17 +3057,17 @@ impl PicturePrimitive {
             let (parent_raster_node_index, parent_allows_subpixel_aa)= {
                 let parent_surface = state.current_surface();
                 (parent_surface.raster_spatial_node_index, parent_surface.allow_subpixel_aa)
             };
             let surface_spatial_node_index = self.spatial_node_index;
 
             // This inflation factor is to be applied to all primitives within the surface.
             let inflation_factor = match composite_mode {
-                PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)) => {
+                PictureCompositeMode::Filter(Filter::Blur(blur_radius)) => {
                     // Only inflate if the caller hasn't already inflated
                     // the bounding rects for this filter.
                     if self.options.inflate_if_required {
                         // The amount of extra space needed for primitives inside
                         // this picture to ensure the visibility check is correct.
                         BLUR_SAMPLE_SCALE * blur_radius
                     } else {
                         0.0
@@ -3125,20 +3185,30 @@ impl PicturePrimitive {
         // If this picture establishes a surface, then map the surface bounding
         // rect into the parent surface coordinate space, and propagate that up
         // to the parent.
         if let Some(ref mut raster_config) = self.raster_config {
             let mut surface_rect = {
                 let surface = state.current_surface_mut();
                 // Inflate the local bounding rect if required by the filter effect.
                 // This inflaction factor is to be applied to the surface itsefl.
+                // TODO: in prepare_for_render we round before multiplying with the
+                // blur sample scale. Should we do this here as well?
                 let inflation_size = match raster_config.composite_mode {
-                    PictureCompositeMode::Filter(FilterOp::Blur(_)) => surface.inflation_factor,
-                    PictureCompositeMode::Filter(FilterOp::DropShadow(_, blur_radius, _)) =>
-                        (blur_radius * BLUR_SAMPLE_SCALE).ceil(),
+                    PictureCompositeMode::Filter(Filter::Blur(_)) => surface.inflation_factor,
+                    PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => {
+                        (shadow.blur_radius * BLUR_SAMPLE_SCALE).ceil()
+                    }
+                    PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                        let mut max = 0.0;
+                        for shadow in shadows {
+                            max = f32::max(max, shadow.blur_radius * BLUR_SAMPLE_SCALE);
+                        }
+                        max.ceil()
+                    }
                     _ => 0.0,
                 };
                 surface.rect = surface.rect.inflate(inflation_size, inflation_size);
                 surface.rect * TypedScale::new(1.0)
             };
 
             // Pop this surface from the stack
             let surface_index = state.pop_surface();
@@ -3156,20 +3226,30 @@ impl PicturePrimitive {
                 {
                     raster_config.establishes_raster_root = false;
                     state.are_raster_roots_assigned = false;
                 }
             }
 
             // Drop shadows draw both a content and shadow rect, so need to expand the local
             // rect of any surfaces to be composited in parent surfaces correctly.
-            if let PictureCompositeMode::Filter(FilterOp::DropShadow(offset, ..)) = raster_config.composite_mode {
-                let content_rect = surface_rect;
-                let shadow_rect = surface_rect.translate(&offset);
-                surface_rect = content_rect.union(&shadow_rect);
+            match raster_config.composite_mode {
+                PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => {
+                    let content_rect = surface_rect;
+                    let shadow_rect = surface_rect.translate(&shadow.offset);
+                    surface_rect = content_rect.union(&shadow_rect);
+                }
+                PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                    for shadow in shadows {
+                        let content_rect = surface_rect;
+                        let shadow_rect = surface_rect.translate(&shadow.offset);
+                        surface_rect = content_rect.union(&shadow_rect);
+                    }
+                }
+                _ => {}
             }
 
             // Propagate up to parent surface, now that we know this surface's static rect
             let parent_surface = state.current_surface_mut();
             parent_surface.map_local_to_surface.set_target_spatial_node(
                 self.spatial_node_index,
                 frame_context.clip_scroll_tree,
             );
@@ -3209,50 +3289,82 @@ impl PicturePrimitive {
         //           to store the same type of data. The exception is the filter
         //           with a ColorMatrix, which stores the color matrix here. It's
         //           probably worth tidying this code up to be a bit more consistent.
         //           Perhaps store the color matrix after the common data, even though
         //           it's not used by that shader.
 
         match raster_config.composite_mode {
             PictureCompositeMode::TileCache { .. } => {}
-            PictureCompositeMode::Filter(FilterOp::Blur(..)) => {}
-            PictureCompositeMode::Filter(FilterOp::DropShadow(offset, _, color)) => {
-                if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
+            PictureCompositeMode::Filter(Filter::Blur(..)) => {}
+            PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => {
+                if self.extra_gpu_data_handles.is_empty() {
+                    self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+                }
+
+                if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
                     // TODO(gw): This is very hacky code below! It stores an extra
                     //           brush primitive below for the special case of a
                     //           drop-shadow where we need a different local
                     //           rect for the shadow. To tidy this up in future,
                     //           we could consider abstracting the code in prim_store.rs
                     //           that writes a brush primitive header.
 
                     // Basic brush primitive header is (see end of prepare_prim_for_render_inner in prim_store.rs)
                     //  [brush specific data]
                     //  [segment_rect, segment data]
-                    let shadow_rect = self.snapped_local_rect.translate(&offset);
+                    let shadow_rect = self.snapped_local_rect.translate(&shadow.offset);
 
                     // ImageBrush colors
-                    request.push(color.premultiplied());
+                    request.push(shadow.color.premultiplied());
                     request.push(PremultipliedColorF::WHITE);
                     request.push([
                         self.snapped_local_rect.size.width,
                         self.snapped_local_rect.size.height,
                         0.0,
                         0.0,
                     ]);
 
                     // segment rect / extra data
                     request.push(shadow_rect);
                     request.push([0.0, 0.0, 0.0, 0.0]);
                 }
             }
+            PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+                for (shadow, extra_handle) in shadows.iter().zip(self.extra_gpu_data_handles.iter_mut()) {
+                    if let Some(mut request) = frame_state.gpu_cache.request(extra_handle) {
+                        // Basic brush primitive header is (see end of prepare_prim_for_render_inner in prim_store.rs)
+                        //  [brush specific data]
+                        //  [segment_rect, segment data]
+                        let shadow_rect = self.snapped_local_rect.translate(&shadow.offset);
+
+                        // ImageBrush colors
+                        request.push(shadow.color.premultiplied());
+                        request.push(PremultipliedColorF::WHITE);
+                        request.push([
+                            self.snapped_local_rect.size.width,
+                            self.snapped_local_rect.size.height,
+                            0.0,
+                            0.0,
+                        ]);
+
+                        // segment rect / extra data
+                        request.push(shadow_rect);
+                        request.push([0.0, 0.0, 0.0, 0.0]);
+                    }
+                }
+            }
             PictureCompositeMode::MixBlend(..) if !frame_context.fb_config.gpu_supports_advanced_blend => {}
             PictureCompositeMode::Filter(ref filter) => {
-                if let FilterOp::ColorMatrix(m) = *filter {
-                    if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
+                if let Filter::ColorMatrix(m) = *filter {
+                    if self.extra_gpu_data_handles.is_empty() {
+                        self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+                    }
+                    if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
                         for i in 0..5 {
                             request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
                         }
                     }
                 }
             }
             PictureCompositeMode::ComponentTransferFilter(handle) => {
                 let filter_data = &mut data_stores.filter_data[handle];
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1,14 +1,14 @@
 /* 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/. */
 
 use api::{BorderRadius, ClipMode, ColorF};
-use api::{FilterOp, ImageRendering, RepeatMode};
+use api::{ImageRendering, RepeatMode};
 use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
 use api::{BoxShadowClipMode, LineStyle, LineOrientation};
 use api::{PrimitiveKeyKind, RasterSpace};
 use api::units::*;
 use crate::border::{get_max_scale_for_border, build_border_instances};
 use crate::border::BorderSegmentCacheKey;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore};
@@ -47,17 +47,17 @@ use crate::scene::SceneProperties;
 use crate::segment::SegmentBuilder;
 use std::{cmp, fmt, hash, ops, u32, usize, mem};
 #[cfg(debug_assertions)]
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::storage;
 use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
 use crate::util::{MatrixHelpers, MaxRect, Recycler};
 use crate::util::{clamp_to_scale_factor, pack_as_float, project_rect, raster_rect_to_device_pixels};
-use crate::internal_types::LayoutPrimitiveInfo;
+use crate::internal_types::{LayoutPrimitiveInfo, Filter};
 use smallvec::SmallVec;
 
 pub mod borders;
 pub mod gradient;
 pub mod image;
 pub mod line_dec;
 pub mod picture;
 pub mod text_run;
@@ -1847,17 +1847,25 @@ impl PrimitiveStore {
                     } else {
                         frame_state.clip_chain_stack.pop_clip();
                     }
 
                     let shadow_rect = match pic.raster_config {
                         Some(ref rc) => match rc.composite_mode {
                             // If we have a drop shadow filter, we also need to include the shadow in
                             // our local rect for the purpose of calculating the size of the picture.
-                            PictureCompositeMode::Filter(FilterOp::DropShadow(offset, ..)) => pic.snapped_local_rect.translate(&offset),
+                            PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => pic.snapped_local_rect.translate(&shadow.offset),
+                            PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                                let mut rect = LayoutRect::zero();
+                                for shadow in shadows {
+                                    rect = rect.union(&pic.snapped_local_rect.translate(&shadow.offset));
+                                }
+
+                                rect
+                            }
                             _ => LayoutRect::zero(),
                         }
                         None => {
                             if let Some(ref rect) = pic_surface_rect {
                                 surface_rect = surface_rect.union(rect);
                             }
                             LayoutRect::zero()
                         }
@@ -2127,29 +2135,44 @@ impl PrimitiveStore {
         // size for the extra GPU cache data handle.
         // TODO(gw): In future, if we support specifying a flag which gets the
         //           stretch size from the segment rect in the shaders, we can
         //           remove this invalidation here completely.
         if let Some(ref raster_config) = pic.raster_config {
             // Inflate the local bounding rect if required by the filter effect.
             // This inflaction factor is to be applied to the surface itself.
             let inflation_size = match raster_config.composite_mode {
-                PictureCompositeMode::Filter(FilterOp::Blur(_)) => surface.inflation_factor,
-                PictureCompositeMode::Filter(FilterOp::DropShadow(_, blur_radius, _)) =>
-                    (blur_radius * BLUR_SAMPLE_SCALE).ceil(),
+                PictureCompositeMode::Filter(Filter::Blur(_)) => surface.inflation_factor,
+                PictureCompositeMode::Filter(Filter::DropShadow(shadow)) => {
+                    (shadow.blur_radius * BLUR_SAMPLE_SCALE).ceil()
+                }
+                PictureCompositeMode::Filter(Filter::DropShadowStack(ref shadows)) => {
+                    let mut max = 0.0;
+                    for shadow in shadows {
+                        max = f32::max(max, shadow.blur_radius * BLUR_SAMPLE_SCALE);
+                    }
+                    max.ceil()
+                }
                 _ => 0.0,
             };
             surface_rect = surface_rect.inflate(inflation_size, inflation_size);
 
             // Layout space for the picture is picture space from the
             // perspective of its child primitives.
             let pic_local_rect = surface_rect * TypedScale::new(1.0);
             if pic.snapped_local_rect != pic_local_rect {
-                if let PictureCompositeMode::Filter(FilterOp::DropShadow(..)) = raster_config.composite_mode {
-                    frame_state.gpu_cache.invalidate(&pic.extra_gpu_data_handle);
+                match raster_config.composite_mode {
+                    PictureCompositeMode::Filter(Filter::DropShadow(..)) 
+                    | PictureCompositeMode::Filter(Filter::DropShadowStack(..))
+                    => {
+                        for handle in &pic.extra_gpu_data_handles {
+                            frame_state.gpu_cache.invalidate(handle);
+                        }
+                    }
+                    _ => {}
                 }
                 // Invalidate any segments built for this picture, since the local
                 // rect has changed.
                 pic.segments_are_valid = false;
                 pic.snapped_local_rect = pic_local_rect;
             }
 
             if let PictureCompositeMode::TileCache { .. } = raster_config.composite_mode {
@@ -2392,17 +2415,17 @@ impl PrimitiveStore {
     // by pushing that opacity value into the color of a primitive
     // if that picture contains one compatible primitive.
     pub fn optimize_picture_if_possible(
         &mut self,
         pic_index: PictureIndex,
     ) {
         // Only handle opacity filters for now.
         let binding = match self.pictures[pic_index.0].requested_composite_mode {
-            Some(PictureCompositeMode::Filter(FilterOp::Opacity(binding, _))) => {
+            Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) => {
                 binding
             }
             _ => {
                 return;
             }
         };
 
         // See if this picture contains a single primitive that supports
--- a/gfx/wr/webrender/src/prim_store/picture.rs
+++ b/gfx/wr/webrender/src/prim_store/picture.rs
@@ -1,21 +1,21 @@
 /* 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/. */
 
 use api::{
-    ColorU, FilterOp, MixBlendMode,
+    ColorU, MixBlendMode,
     PropertyBinding, PropertyBindingId,
 };
 use api::units::{Au, LayoutSize, LayoutVector2D};
 use crate::intern::ItemUid;
 use crate::display_list_flattener::IsVisible;
 use crate::intern::{Internable, InternDebug, Handle as InternHandle};
-use crate::internal_types::LayoutPrimitiveInfo;
+use crate::internal_types::{LayoutPrimitiveInfo, Filter};
 use crate::picture::PictureCompositeMode;
 use crate::prim_store::{
     PrimKey, PrimKeyCommonData, PrimTemplate, PrimTemplateCommonData,
     PrimitiveInstanceKind, PrimitiveSceneData, PrimitiveStore, VectorKey,
     InternablePrimitive,
 };
 
 /// Represents a hashable description of how a picture primitive
@@ -34,16 +34,17 @@ pub enum PictureCompositeKey {
     Grayscale(Au),
     HueRotate(Au),
     Invert(Au),
     Opacity(Au),
     OpacityBinding(PropertyBindingId, Au),
     Saturate(Au),
     Sepia(Au),
     DropShadow(VectorKey, Au, ColorU),
+    DropShadowStack(Vec<(VectorKey, Au, ColorU)>),
     ColorMatrix([Au; 20]),
     SrgbToLinear,
     LinearToSrgb,
     ComponentTransfer(ItemUid),
 
     // MixBlendMode
     Multiply,
     Screen,
@@ -82,48 +83,55 @@ impl From<Option<PictureCompositeMode>> 
                     MixBlendMode::Hue => PictureCompositeKey::Hue,
                     MixBlendMode::Saturation => PictureCompositeKey::Saturation,
                     MixBlendMode::Color => PictureCompositeKey::Color,
                     MixBlendMode::Luminosity => PictureCompositeKey::Luminosity,
                 }
             }
             Some(PictureCompositeMode::Filter(op)) => {
                 match op {
-                    FilterOp::Blur(value) => PictureCompositeKey::Blur(Au::from_f32_px(value)),
-                    FilterOp::Brightness(value) => PictureCompositeKey::Brightness(Au::from_f32_px(value)),
-                    FilterOp::Contrast(value) => PictureCompositeKey::Contrast(Au::from_f32_px(value)),
-                    FilterOp::Grayscale(value) => PictureCompositeKey::Grayscale(Au::from_f32_px(value)),
-                    FilterOp::HueRotate(value) => PictureCompositeKey::HueRotate(Au::from_f32_px(value)),
-                    FilterOp::Invert(value) => PictureCompositeKey::Invert(Au::from_f32_px(value)),
-                    FilterOp::Saturate(value) => PictureCompositeKey::Saturate(Au::from_f32_px(value)),
-                    FilterOp::Sepia(value) => PictureCompositeKey::Sepia(Au::from_f32_px(value)),
-                    FilterOp::SrgbToLinear => PictureCompositeKey::SrgbToLinear,
-                    FilterOp::LinearToSrgb => PictureCompositeKey::LinearToSrgb,
-                    FilterOp::Identity => PictureCompositeKey::Identity,
-                    FilterOp::DropShadow(offset, radius, color) => {
-                        PictureCompositeKey::DropShadow(offset.into(), Au::from_f32_px(radius), color.into())
+                    Filter::Blur(value) => PictureCompositeKey::Blur(Au::from_f32_px(value)),
+                    Filter::Brightness(value) => PictureCompositeKey::Brightness(Au::from_f32_px(value)),
+                    Filter::Contrast(value) => PictureCompositeKey::Contrast(Au::from_f32_px(value)),
+                    Filter::Grayscale(value) => PictureCompositeKey::Grayscale(Au::from_f32_px(value)),
+                    Filter::HueRotate(value) => PictureCompositeKey::HueRotate(Au::from_f32_px(value)),
+                    Filter::Invert(value) => PictureCompositeKey::Invert(Au::from_f32_px(value)),
+                    Filter::Saturate(value) => PictureCompositeKey::Saturate(Au::from_f32_px(value)),
+                    Filter::Sepia(value) => PictureCompositeKey::Sepia(Au::from_f32_px(value)),
+                    Filter::SrgbToLinear => PictureCompositeKey::SrgbToLinear,
+                    Filter::LinearToSrgb => PictureCompositeKey::LinearToSrgb,
+                    Filter::Identity => PictureCompositeKey::Identity,
+                    Filter::DropShadowStack(ref shadows) => {
+                        PictureCompositeKey::DropShadowStack(
+                            shadows.iter().map(|shadow| {
+                                (shadow.offset.into(), Au::from_f32_px(shadow.blur_radius), shadow.color.into())
+                            }).collect()
+                        )
                     }
-                    FilterOp::Opacity(binding, _) => {
+                    Filter::DropShadow(shadow) => {
+                        PictureCompositeKey::DropShadow(shadow.offset.into(), Au::from_f32_px(shadow.blur_radius), shadow.color.into())
+                    }
+                    Filter::Opacity(binding, _) => {
                         match binding {
                             PropertyBinding::Value(value) => {
                                 PictureCompositeKey::Opacity(Au::from_f32_px(value))
                             }
                             PropertyBinding::Binding(key, default) => {
                                 PictureCompositeKey::OpacityBinding(key.id, Au::from_f32_px(default))
                             }
                         }
                     }
-                    FilterOp::ColorMatrix(values) => {
+                    Filter::ColorMatrix(values) => {
                         let mut quantized_values: [Au; 20] = [Au(0); 20];
                         for (value, result) in values.iter().zip(quantized_values.iter_mut()) {
                             *result = Au::from_f32_px(*value);
                         }
                         PictureCompositeKey::ColorMatrix(quantized_values)
                     }
-                    FilterOp::ComponentTransfer => unreachable!(),
+                    Filter::ComponentTransfer => unreachable!(),
                 }
             }
             Some(PictureCompositeMode::ComponentTransferFilter(handle)) => {
                 PictureCompositeKey::ComponentTransfer(handle.uid())
             }
             Some(PictureCompositeMode::Blit(_)) |
             Some(PictureCompositeMode::TileCache { .. }) |
             None => {
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -671,16 +671,42 @@ pub enum ClearMode {
     One,
     /// This task doesn't care what it is cleared to - it will completely overwrite it.
     DontCare,
 
     // Applicable to color targets only.
     Transparent,
 }
 
+/// In order to avoid duplicating the down-scaling and blur passes when a picture has several blurs,
+/// we use a local (primitive-level) cache of the render tasks generated for a single shadowed primitive
+/// in a single frame.
+pub type BlurTaskCache = FastHashMap<BlurTaskKey, RenderTaskId>;
+
+/// Since we only use it within a single primitive, the key only needs to contain the down-scaling level
+/// and the blur std deviation.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum BlurTaskKey {
+    DownScale(u32),
+    Blur { downscale_level: u32, stddev_x: u32, stddev_y: u32 },
+}
+
+impl BlurTaskKey {
+    fn downscale_and_blur(downscale_level: u32, blur_stddev: DeviceSize) -> Self {
+        // Quantise the std deviations and store it as integers to work around
+        // Eq and Hash's f32 allergy.
+        // The blur radius is rounded before RenderTask::new_blur so we don't need
+        // a lot of precision.
+        const QUANTIZATION_FACTOR: f32 = 1024.0;
+        let stddev_x = (blur_stddev.width * QUANTIZATION_FACTOR) as u32;
+        let stddev_y = (blur_stddev.height * QUANTIZATION_FACTOR) as u32;
+        BlurTaskKey::Blur { downscale_level, stddev_x, stddev_y }
+    }
+}
+
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTask {
     pub location: RenderTaskLocation,
     pub children: Vec<RenderTaskId>,
     pub kind: RenderTaskKind,
     pub clear_mode: ClearMode,
@@ -900,25 +926,24 @@ impl RenderTask {
                                 info.minimal_shadow_rect.origin,
                                 device_pixel_scale,
                                 fb_config,
                             );
 
                             let mask_task_id = render_tasks.add(mask_task);
 
                             // Blur it
-                            let blur_render_task = RenderTask::new_blur(
+                            RenderTask::new_blur(
                                 DeviceSize::new(blur_radius_dp, blur_radius_dp),
                                 mask_task_id,
                                 render_tasks,
                                 RenderTargetKind::Alpha,
                                 ClearMode::Zero,
-                            );
-
-                            render_tasks.add(blur_render_task)
+                                None,
+                            )
                         }
                     ));
                 }
                 ClipItem::Rectangle(_, ClipMode::Clip) => {
                     if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) {
                         // This is conservative - it's only the case that we actually need
                         // a clear here if we end up adding this mask via add_tiled_clip_mask,
                         // but for simplicity we will just clear if any of these are encountered,
@@ -1018,69 +1043,103 @@ impl RenderTask {
     //           +---- This is stored as the input task to the primitive shader.
     //
     pub fn new_blur(
         blur_std_deviation: DeviceSize,
         src_task_id: RenderTaskId,
         render_tasks: &mut RenderTaskTree,
         target_kind: RenderTargetKind,
         clear_mode: ClearMode,
-    ) -> Self {
+        mut blur_cache: Option<&mut BlurTaskCache>,
+    ) -> RenderTaskId {
         // Adjust large std deviation value.
         let mut adjusted_blur_std_deviation = blur_std_deviation;
         let (blur_target_size, uv_rect_kind) = {
             let src_task = &render_tasks[src_task_id];
             (src_task.get_dynamic_size(), src_task.uv_rect_kind())
         };
         let mut adjusted_blur_target_size = blur_target_size;
         let mut downscaling_src_task_id = src_task_id;
         let mut scale_factor = 1.0;
+        let mut n_downscales = 1;
         while adjusted_blur_std_deviation.width > MAX_BLUR_STD_DEVIATION &&
               adjusted_blur_std_deviation.height > MAX_BLUR_STD_DEVIATION {
             if adjusted_blur_target_size.width < MIN_DOWNSCALING_RT_SIZE ||
                adjusted_blur_target_size.height < MIN_DOWNSCALING_RT_SIZE {
                 break;
             }
             adjusted_blur_std_deviation = adjusted_blur_std_deviation * 0.5;
             scale_factor *= 2.0;
             adjusted_blur_target_size = (blur_target_size.to_f32() / scale_factor).to_i32();
-            let downscaling_task = RenderTask::new_scaling(
-                downscaling_src_task_id,
-                render_tasks,
-                target_kind,
-                adjusted_blur_target_size,
-            );
-            downscaling_src_task_id = render_tasks.add(downscaling_task);
+
+            let cached_task = match blur_cache {
+                Some(ref mut cache) => cache.get(&BlurTaskKey::DownScale(n_downscales)).cloned(),
+                None => None,
+            };
+
+            downscaling_src_task_id = cached_task.unwrap_or_else(|| {
+                let downscaling_task = RenderTask::new_scaling(
+                    downscaling_src_task_id,
+                    render_tasks,
+                    target_kind,
+                    adjusted_blur_target_size,
+                );
+                render_tasks.add(downscaling_task)
+            });
+
+            if let Some(ref mut cache) = blur_cache {
+                cache.insert(BlurTaskKey::DownScale(n_downscales), downscaling_src_task_id);
+            }
+
+            n_downscales += 1;
         }
 
-        let blur_task_v = RenderTask::with_dynamic_location(
-            adjusted_blur_target_size,
-            vec![downscaling_src_task_id],
-            RenderTaskKind::VerticalBlur(BlurTask {
-                blur_std_deviation: adjusted_blur_std_deviation.height,
-                target_kind,
-                uv_rect_handle: GpuCacheHandle::new(),
-                uv_rect_kind,
-            }),
-            clear_mode,
-        );
+
+        let blur_key = BlurTaskKey::downscale_and_blur(n_downscales, adjusted_blur_std_deviation);
+
+        let cached_task = match blur_cache {
+            Some(ref mut cache) => cache.get(&blur_key).cloned(),
+            None => None,
+        };
+
+        let blur_task_id = cached_task.unwrap_or_else(|| {
+            let blur_task_v = RenderTask::with_dynamic_location(
+                adjusted_blur_target_size,
+                vec![downscaling_src_task_id],
+                RenderTaskKind::VerticalBlur(BlurTask {
+                    blur_std_deviation: adjusted_blur_std_deviation.height,
+                    target_kind,
+                    uv_rect_handle: GpuCacheHandle::new(),
+                    uv_rect_kind,
+                }),
+                clear_mode,
+            );
 
-        let blur_task_v_id = render_tasks.add(blur_task_v);
+            let blur_task_v_id = render_tasks.add(blur_task_v);
 
-        RenderTask::with_dynamic_location(
-            adjusted_blur_target_size,
-            vec![blur_task_v_id],
-            RenderTaskKind::HorizontalBlur(BlurTask {
-                blur_std_deviation: adjusted_blur_std_deviation.width,
-                target_kind,
-                uv_rect_handle: GpuCacheHandle::new(),
-                uv_rect_kind,
-            }),
-            clear_mode,
-        )
+            let blur_task_h = RenderTask::with_dynamic_location(
+                adjusted_blur_target_size,
+                vec![blur_task_v_id],
+                RenderTaskKind::HorizontalBlur(BlurTask {
+                    blur_std_deviation: adjusted_blur_std_deviation.width,
+                    target_kind,
+                    uv_rect_handle: GpuCacheHandle::new(),
+                    uv_rect_kind,
+                }),
+                clear_mode,
+            );
+
+            render_tasks.add(blur_task_h)
+        });
+
+        if let Some(ref mut cache) = blur_cache {
+            cache.insert(blur_key, blur_task_id);
+        }
+
+        blur_task_id
     }
 
     pub fn new_border_segment(
         size: DeviceIntSize,
         instances: Vec<BorderInstance>,
     ) -> Self {
         RenderTask::with_dynamic_location(
             size,
@@ -1388,17 +1447,16 @@ impl RenderTask {
             RenderTaskKind::Test(..) => {
                 panic!("RenderTask tests aren't expected to exercise this code");
             }
         };
 
         if let Some(mut request) = gpu_cache.request(cache_handle) {
             let p0 = target_rect.origin.to_f32();
             let p1 = target_rect.bottom_right().to_f32();
-
             let image_source = ImageSource {
                 p0,
                 p1,
                 texture_layer: target_index.0 as f32,
                 user_data: [0.0; 3],
                 uv_rect_kind,
             };
             image_source.write_gpu_blocks(&mut request);
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.rs
@@ -1,17 +1,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/. */
 
 use api::{BuiltDisplayList, ColorF, DynamicProperties, Epoch};
 use api::{FilterOp, TempFilterData, FilterData, ComponentTransferFuncType};
 use api::{PipelineId, PropertyBinding, PropertyBindingId, ItemRange, MixBlendMode, StackingContext};
 use api::units::{LayoutSize, LayoutTransform};
-use crate::internal_types::FastHashMap;
+use crate::internal_types::{FastHashMap, Filter};
 use std::sync::Arc;
 
 /// Stores a map of the animated property bindings for the current display list. These
 /// can be used to animate the transform and/or opacity of a display list without
 /// re-submitting the display list itself.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct SceneProperties {
@@ -198,83 +198,23 @@ impl Scene {
         if let Some(ref root_id) = self.root_pipeline_id {
             return self.pipelines.contains_key(root_id);
         }
 
         false
     }
 }
 
-/// An arbitrary number which we assume opacity is invisible below.
-pub const OPACITY_EPSILON: f32 = 0.001;
-
-pub trait FilterOpHelpers {
-    fn is_visible(&self) -> bool;
-    fn is_noop(&self) -> bool;
-}
-
-impl FilterOpHelpers for FilterOp {
-    fn is_visible(&self) -> bool {
-        match *self {
-            FilterOp::Identity |
-            FilterOp::Blur(..) |
-            FilterOp::Brightness(..) |
-            FilterOp::Contrast(..) |
-            FilterOp::Grayscale(..) |
-            FilterOp::HueRotate(..) |
-            FilterOp::Invert(..) |
-            FilterOp::Saturate(..) |
-            FilterOp::Sepia(..) |
-            FilterOp::DropShadow(..) |
-            FilterOp::ColorMatrix(..) |
-            FilterOp::SrgbToLinear |
-            FilterOp::LinearToSrgb |
-            FilterOp::ComponentTransfer  => true,
-            FilterOp::Opacity(_, amount) => {
-                amount > OPACITY_EPSILON
-            }
-        }
-    }
-
-    fn is_noop(&self) -> bool {
-        match *self {
-            FilterOp::Identity => false, // this is intentional
-            FilterOp::Blur(length) => length == 0.0,
-            FilterOp::Brightness(amount) => amount == 1.0,
-            FilterOp::Contrast(amount) => amount == 1.0,
-            FilterOp::Grayscale(amount) => amount == 0.0,
-            FilterOp::HueRotate(amount) => amount == 0.0,
-            FilterOp::Invert(amount) => amount == 0.0,
-            FilterOp::Opacity(_, amount) => amount >= 1.0,
-            FilterOp::Saturate(amount) => amount == 1.0,
-            FilterOp::Sepia(amount) => amount == 0.0,
-            FilterOp::DropShadow(offset, blur, _) => {
-                offset.x == 0.0 && offset.y == 0.0 && blur == 0.0
-            },
-            FilterOp::ColorMatrix(matrix) => {
-                matrix == [1.0, 0.0, 0.0, 0.0,
-                           0.0, 1.0, 0.0, 0.0,
-                           0.0, 0.0, 1.0, 0.0,
-                           0.0, 0.0, 0.0, 1.0,
-                           0.0, 0.0, 0.0, 0.0]
-            }
-            FilterOp::SrgbToLinear |
-            FilterOp::LinearToSrgb |
-            FilterOp::ComponentTransfer => false,
-        }
-    }
-}
-
 pub trait StackingContextHelpers {
     fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode>;
     fn filter_ops_for_compositing(
         &self,
         display_list: &BuiltDisplayList,
         input_filters: ItemRange<FilterOp>,
-    ) -> Vec<FilterOp>;
+    ) -> Vec<Filter>;
     fn filter_datas_for_compositing(
         &self,
         display_list: &BuiltDisplayList,
         input_filter_datas: &[TempFilterData],
     ) -> Vec<FilterData>;
 }
 
 impl StackingContextHelpers for StackingContext {
@@ -284,23 +224,23 @@ impl StackingContextHelpers for Stacking
             _ => Some(self.mix_blend_mode),
         }
     }
 
     fn filter_ops_for_compositing(
         &self,
         display_list: &BuiltDisplayList,
         input_filters: ItemRange<FilterOp>,
-    ) -> Vec<FilterOp> {
+    ) -> Vec<Filter> {
         // TODO(gw): Now that we resolve these later on,
         //           we could probably make it a bit
         //           more efficient than cloning these here.
         let mut filters = vec![];
         for filter in display_list.get(input_filters) {
-            filters.push(filter);
+            filters.push(filter.into());
         }
         filters
     }
 
     fn filter_datas_for_compositing(
         &self,
         display_list: &BuiltDisplayList,
         input_filter_datas: &[TempFilterData],
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -1,29 +1,29 @@
 /* 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/. */
 
 use api::{ColorF, BorderStyle, MixBlendMode, PipelineId, PremultipliedColorF};
-use api::{DocumentLayer, FilterData, FilterOp, ImageFormat, LineOrientation};
+use api::{DocumentLayer, FilterData, ImageFormat, LineOrientation};
 use api::units::*;
 #[cfg(feature = "pathfinder")]
 use api::FontRenderMode;
 use crate::batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image, BatchBuilder};
 use crate::clip::ClipStore;
 use crate::clip_scroll_tree::{ClipScrollTree};
 use crate::debug_render::DebugItem;
 use crate::device::{Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use crate::frame_builder::FrameGlobalResources;
 use crate::gpu_cache::{GpuCache};
 use crate::gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
 use crate::gpu_types::{TransformData, TransformPalette, ZBufferIdGenerator};
-use crate::internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource};
+use crate::internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource, Filter};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use crate::picture::{RecordedDirtyRegion, SurfaceInfo};
 use crate::prim_store::gradient::GRADIENT_FP_STOPS;
 use crate::prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
 use crate::profiler::FrameProfileCounters;
 use crate::render_backend::{DataStores, FrameId};
 use crate::render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind};
@@ -1155,25 +1155,25 @@ impl RenderPass {
             }
         }
     }
 }
 
 #[derive(Debug, Clone, Default)]
 pub struct CompositeOps {
     // Requires only a single texture as input (e.g. most filters)
-    pub filters: Vec<FilterOp>,
+    pub filters: Vec<Filter>,
     pub filter_datas: Vec<FilterData>,
 
     // Requires two source textures (e.g. mix-blend-mode)
     pub mix_blend_mode: Option<MixBlendMode>,
 }
 
 impl CompositeOps {
-    pub fn new(filters: Vec<FilterOp>,
+    pub fn new(filters: Vec<Filter>,
                filter_datas: Vec<FilterData>,
                mix_blend_mode: Option<MixBlendMode>) -> Self {
         CompositeOps {
             filters,
             filter_datas,
             mix_blend_mode,
         }
     }
--- a/gfx/wr/webrender_api/src/display_item.rs
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -516,25 +516,25 @@ pub struct BoxShadowDisplayItem {
     pub border_radius: BorderRadius,
     pub clip_mode: BoxShadowClipMode,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct PushShadowDisplayItem {
     pub space_and_clip: SpaceAndClipInfo,
     pub shadow: Shadow,
+    pub should_inflate: bool,
 }
 
 #[repr(C)]
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct Shadow {
     pub offset: LayoutVector2D,
     pub color: ColorF,
     pub blur_radius: f32,
-    pub should_inflate: bool,
 }
 
 #[repr(u8)]
 #[derive(Debug, Copy, Clone, Hash, Eq, MallocSizeOf, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
 pub enum ExtendMode {
     Clamp,
     Repeat,
 }
@@ -707,41 +707,23 @@ pub enum FilterOp {
     Brightness(f32),
     Contrast(f32),
     Grayscale(f32),
     HueRotate(f32),
     Invert(f32),
     Opacity(PropertyBinding<f32>, f32),
     Saturate(f32),
     Sepia(f32),
-    DropShadow(LayoutVector2D, f32, ColorF),
+    DropShadow(Shadow),
     ColorMatrix([f32; 20]),
     SrgbToLinear,
     LinearToSrgb,
     ComponentTransfer,
 }
 
-impl FilterOp {
-    /// Ensure that the parameters for a filter operation
-    /// are sensible.
-    pub fn sanitize(&self) -> FilterOp {
-        match self {
-            FilterOp::Blur(radius) => {
-                let radius = radius.min(MAX_BLUR_RADIUS);
-                FilterOp::Blur(radius)
-            }
-            FilterOp::DropShadow(offset, radius, color) => {
-                let radius = radius.min(MAX_BLUR_RADIUS);
-                FilterOp::DropShadow(*offset, radius, *color)
-            }
-            filter => filter.clone(),
-        }
-    }
-}
-
 #[repr(u8)]
 #[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
 pub enum ComponentTransferFuncType {
   Identity = 0,
   Table = 1,
   Discrete = 2,
   Linear = 3,
   Gamma = 4,
--- a/gfx/wr/webrender_api/src/display_list.rs
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -1635,20 +1635,22 @@ impl DisplayListBuilder {
         });
         self.push_item(&item);
     }
 
     pub fn push_shadow(
         &mut self,
         space_and_clip: &di::SpaceAndClipInfo,
         shadow: di::Shadow,
+        should_inflate: bool,
     ) {
         let item = di::DisplayItem::PushShadow(di::PushShadowDisplayItem {
             space_and_clip: *space_and_clip,
             shadow,
+            should_inflate,
         });
         self.push_item(&item);
     }
 
     pub fn pop_all_shadows(&mut self) {
         self.push_item(&di::DisplayItem::PopAllShadows);
     }
 
--- a/gfx/wr/wrench/src/rawtest.rs
+++ b/gfx/wr/wrench/src/rawtest.rs
@@ -947,18 +947,18 @@ impl<'a> RawtestHarness<'a> {
                     ColorF::new(0.0, 1.0, 0.0, 1.0),
                 );
                 builder.push_shadow(
                     &space_and_clip,
                     Shadow {
                         offset: LayoutVector2D::new(1.0, 1.0),
                         blur_radius: 1.0,
                         color: ColorF::new(0.0, 0.0, 0.0, 1.0),
-                        should_inflate: true,
                     },
+                    true,
                 );
                 let info = CommonItemProperties {
                     clip_rect: rect(110., 110., 50., 2.),
                     clip_id,
                     spatial_id,
                     is_backface_visible: true,
                     hit_info: None,
                 };
@@ -1025,18 +1025,18 @@ impl<'a> RawtestHarness<'a> {
             };
 
             builder.push_shadow(
                 &SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id),
                 Shadow {
                     offset: LayoutVector2D::new(1.0, 1.0),
                     blur_radius: 1.0,
                     color: shadow_color,
-                    should_inflate: true,
                 },
+                true,
             );
             let info = self.make_common_properties(rect(110., 110., 50., 2.));
             builder.push_line(
                 &info,
                 &info.clip_rect,
                 0.0, LineOrientation::Horizontal,
                 &ColorF::new(0.0, 0.0, 0.0, 1.0),
                 LineStyle::Solid,
--- a/gfx/wr/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wr/wrench/src/yaml_frame_reader.rs
@@ -1691,18 +1691,18 @@ impl YamlFrameReader {
         let color = yaml["color"].as_colorf().unwrap_or(ColorF::BLACK);
 
         dl.push_shadow(
             &SpaceAndClipInfo { spatial_id: info.spatial_id, clip_id: info.clip_id },
             Shadow {
                 blur_radius,
                 offset,
                 color,
-                should_inflate: true,
             },
+            true,
         );
     }
 
     fn handle_pop_all_shadows(&mut self, dl: &mut DisplayListBuilder) {
         dl.pop_all_shadows();
     }
 
     fn handle_clip_chain(&mut self, builder: &mut DisplayListBuilder, yaml: &Yaml) {
--- a/gfx/wr/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wr/wrench/src/yaml_frame_writer.rs
@@ -233,16 +233,25 @@ fn write_reference_frame(
             ReferenceFrameKind::Perspective { .. } => "perspective",
         },
         &properties.resolve_layout_transform(&reference_frame.transform)
     );
 
     usize_node(parent, "id", clip_id_mapper.add_spatial_id(reference_frame.id));
 }
 
+fn shadow_parameters(shadow: &Shadow) -> String {
+    format!(
+        "[{},{}],{},[{}]",
+        shadow.offset.x, shadow.offset.y,
+        shadow.blur_radius,
+        color_to_string(shadow.color)
+    )
+}
+
 fn write_stacking_context(
     parent: &mut Table,
     sc: &StackingContext,
     properties: &SceneProperties,
     filter_iter: AuxIter<FilterOp>,
     filter_data_iter: &[TempFilterData],
     display_list: &BuiltDisplayList,
 ) {
@@ -274,21 +283,21 @@ fn write_stacking_context(
             FilterOp::HueRotate(x) => { filters.push(Yaml::String(format!("hue-rotate({})", x))) }
             FilterOp::Invert(x) => { filters.push(Yaml::String(format!("invert({})", x))) }
             FilterOp::Opacity(x, _) => {
                 filters.push(Yaml::String(format!("opacity({})",
                                                   properties.resolve_float(&x))))
             }
             FilterOp::Saturate(x) => { filters.push(Yaml::String(format!("saturate({})", x))) }
             FilterOp::Sepia(x) => { filters.push(Yaml::String(format!("sepia({})", x))) }
-            FilterOp::DropShadow(offset, blur, color) => {
-                filters.push(Yaml::String(format!("drop-shadow([{},{}],{},[{}])",
-                                                  offset.x, offset.y,
-                                                  blur,
-                                                  color_to_string(color))))
+            FilterOp::DropShadow(shadow) => {
+                filters.push(Yaml::String(format!(
+                    "drop-shadow({})",
+                    shadow_parameters(&shadow)
+                )))
             }
             FilterOp::ColorMatrix(matrix) => {
                 filters.push(Yaml::String(format!("color-matrix({:?})", matrix)))
             }
             FilterOp::SrgbToLinear => {
                 filters.push(Yaml::String("srgb-to-linear".to_string()))
             }
             FilterOp::LinearToSrgb => {
--- a/gfx/wr/wrench/src/yaml_helper.rs
+++ b/gfx/wr/wrench/src/yaml_helper.rs
@@ -596,19 +596,21 @@ impl YamlHelper for Yaml {
                     Some(FilterOp::Sepia(args[0].parse().unwrap()))
                 }
                 ("srgb-to-linear", _, _)  => Some(FilterOp::SrgbToLinear),
                 ("linear-to-srgb", _, _)  => Some(FilterOp::LinearToSrgb),
                 ("drop-shadow", ref args, _) if args.len() == 3 => {
                     let str = format!("---\noffset: {}\nblur-radius: {}\ncolor: {}\n", args[0], args[1], args[2]);
                     let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse drop-shadow");
                     let yaml = yaml_doc.pop().unwrap();
-                    Some(FilterOp::DropShadow(yaml["offset"].as_vector().unwrap(),
-                                              yaml["blur-radius"].as_f32().unwrap(),
-                                              yaml["color"].as_colorf().unwrap()))
+                    Some(FilterOp::DropShadow(Shadow {
+                        offset: yaml["offset"].as_vector().unwrap(),
+                        blur_radius: yaml["blur-radius"].as_f32().unwrap(),
+                        color: yaml["color"].as_colorf().unwrap()
+                    }))
                 }
                 ("color-matrix", ref args, _) if args.len() == 20 => {
                     let m: Vec<f32> = args.iter().map(|f| f.parse().unwrap()).collect();
                     let mut matrix: [f32; 20] = [0.0; 20];
                     matrix.clone_from_slice(&m);
                     Some(FilterOp::ColorMatrix(matrix))
                 }
                 (_, _, _) => None,
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -6109,29 +6109,28 @@ void nsLayoutUtils::PaintTextShadow(
     nsContextBoxBlur contextBoxBlur;
 
     nscolor shadowColor = shadowDetails->mColor.CalcColor(aForegroundColor);
 
     // Webrender just needs the shadow details
     if (auto* textDrawer = aContext->GetTextDrawer()) {
       wr::Shadow wrShadow;
 
-      // Gecko already inflates the bounding rect of text shadows,
-      // so tell WR not to inflate again.
-      wrShadow.should_inflate = false;
-
       wrShadow.offset = {
           presCtx->AppUnitsToFloatDevPixels(shadowDetails->mXOffset),
           presCtx->AppUnitsToFloatDevPixels(shadowDetails->mYOffset)};
 
       wrShadow.blur_radius =
           presCtx->AppUnitsToFloatDevPixels(shadowDetails->mRadius);
       wrShadow.color = wr::ToColorF(ToDeviceColor(shadowColor));
 
-      textDrawer->AppendShadow(wrShadow);
+      // Gecko already inflates the bounding rect of text shadows,
+      // so tell WR not to inflate again.
+      bool inflate = false;
+      textDrawer->AppendShadow(wrShadow, inflate);
       continue;
     }
 
     gfxContext* shadowContext = contextBoxBlur.Init(
         shadowRect, 0, blurRadius, presCtx->AppUnitsPerDevPixel(), aDestCtx,
         aDirtyRect, nullptr,
         nsContextBoxBlur::DISABLE_HARDWARE_ACCELERATION_BLUR);
     if (!shadowContext) continue;
--- a/layout/generic/TextDrawTarget.h
+++ b/layout/generic/TextDrawTarget.h
@@ -223,18 +223,19 @@ class TextDrawTarget : public DrawTarget
     rect = rect.Intersect(mClipStack.LastElement());
     mClipStack.AppendElement(rect);
   }
 
   void PopClip() override { mClipStack.RemoveLastElement(); }
 
   IntSize GetSize() const override { return mSize; }
 
-  void AppendShadow(const wr::Shadow& aShadow) {
-    mBuilder.PushShadow(mBoundsRect, ClipRect(), mBackfaceVisible, aShadow);
+  void AppendShadow(const wr::Shadow& aShadow, bool aInflate) {
+    mBuilder.PushShadow(mBoundsRect, ClipRect(), mBackfaceVisible, aShadow,
+                        aInflate);
     mHasShadows = true;
   }
 
   void TerminateShadows() {
     if (mHasShadows) {
       mBuilder.PopAllShadows();
       mHasShadows = false;
     }
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -5854,28 +5854,25 @@ void nsTextFrame::PaintOneShadow(const P
   nscoord blurRadius = std::max(aShadowDetails->mRadius, 0);
 
   nscolor shadowColor =
       aShadowDetails->mColor.CalcColor(aParams.foregroundColor);
 
   if (auto* textDrawer = aParams.context->GetTextDrawer()) {
     wr::Shadow wrShadow;
 
-    // Gecko already inflates the bounding rect of text shadows,
-    // so tell WR not to inflate again.
-    wrShadow.should_inflate = true;
-
     wrShadow.offset = {
         PresContext()->AppUnitsToFloatDevPixels(aShadowDetails->mXOffset),
         PresContext()->AppUnitsToFloatDevPixels(aShadowDetails->mYOffset)};
 
     wrShadow.blur_radius = PresContext()->AppUnitsToFloatDevPixels(blurRadius);
     wrShadow.color = wr::ToColorF(ToDeviceColor(shadowColor));
 
-    textDrawer->AppendShadow(wrShadow);
+    bool inflate = true;
+    textDrawer->AppendShadow(wrShadow, inflate);
     return;
   }
 
   // This rect is the box which is equivalent to where the shadow will be
   // painted. The origin of aBoundingBox is the text baseline left, so we must
   // translate it by that much in order to make the origin the top-left corner
   // of the text bounding box. Note that aLeftSideOffset is line-left, so
   // actually means top offset in vertical writing modes.
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -9946,28 +9946,25 @@ bool nsDisplayFilters::CreateWebRenderCS
               "Exactly one drop shadow should have been "
               "parsed.");
           return false;
         }
 
         nsCSSShadowItem* shadow = shadows->ShadowAt(0);
         nscolor color = shadow->mColor.CalcColor(mFrame);
 
-        auto filterOp = wr::FilterOp::DropShadow(
-            {
-                NSAppUnitsToFloatPixels(shadow->mXOffset, appUnitsPerDevPixel),
-                NSAppUnitsToFloatPixels(shadow->mYOffset, appUnitsPerDevPixel),
-            },
-            NSAppUnitsToFloatPixels(shadow->mRadius, appUnitsPerDevPixel),
-            {
-                NS_GET_R(color) / 255.0f,
-                NS_GET_G(color) / 255.0f,
-                NS_GET_B(color) / 255.0f,
-                NS_GET_A(color) / 255.0f,
-            });
+        wr::Shadow wrShadow;
+        wrShadow.offset = {
+          NSAppUnitsToFloatPixels(shadow->mXOffset, appUnitsPerDevPixel),
+          NSAppUnitsToFloatPixels(shadow->mYOffset, appUnitsPerDevPixel)};
+        wrShadow.blur_radius =
+          NSAppUnitsToFloatPixels(shadow->mRadius, appUnitsPerDevPixel);
+        wrShadow.color = {NS_GET_R(color) / 255.0f, NS_GET_G(color) / 255.0f,
+                          NS_GET_B(color) / 255.0f, NS_GET_A(color) / 255.0f};
+        auto filterOp = wr::FilterOp::DropShadow(wrShadow);
 
         wrFilters.filters.AppendElement(filterOp);
         break;
       }
       default:
         return false;
     }
   }
--- a/layout/svg/nsFilterInstance.cpp
+++ b/layout/svg/nsFilterInstance.cpp
@@ -259,27 +259,27 @@ bool nsFilterInstance::BuildWebRenderFil
 
       const DropShadowAttributes& shadow = attr.as<DropShadowAttributes>();
 
       const Size& stdDev = shadow.mStdDeviation;
       if (stdDev.width != stdDev.height) {
         return false;
       }
 
-      wr::LayoutVector2D offset = {(float)shadow.mOffset.x,
-                                   (float)shadow.mOffset.y};
-      float radius = stdDev.width;
       Color color = shadow.mColor;
       if (!primNeedsSrgb) {
         color = Color(gsRGBToLinearRGBMap[uint8_t(color.r * 255)],
                       gsRGBToLinearRGBMap[uint8_t(color.g * 255)],
                       gsRGBToLinearRGBMap[uint8_t(color.b * 255)], color.a);
       }
-      wr::FilterOp filterOp = wr::FilterOp::DropShadow(
-          offset, radius, wr::ToColorF(ToDeviceColor(color)));
+      wr::Shadow wrShadow;
+      wrShadow.offset = {(float)shadow.mOffset.x, (float)shadow.mOffset.y};
+      wrShadow.color = wr::ToColorF(ToDeviceColor(color));
+      wrShadow.blur_radius = stdDev.width;
+      wr::FilterOp filterOp = wr::FilterOp::DropShadow(wrShadow);
 
       aWrFilters.filters.AppendElement(filterOp);
     } else if (attr.is<ComponentTransferAttributes>()) {
       const ComponentTransferAttributes& attributes =
           attr.as<ComponentTransferAttributes>();
 
       size_t numValues =
           attributes.mValues[0].Length() + attributes.mValues[1].Length() +