Bug 1555483 - Part 1: Add SVG filter primitive display item. r=gw
authorConnor Brewster <cbrewster@mozilla.com>
Wed, 10 Jul 2019 22:36:25 +0000
changeset 482307 31009157a5b4152b24b194b6efc10685a3843e6e
parent 482306 9e4404e0c26708e2b8b4b8965003df609d068319
child 482308 6c311d8365d2d857f8dcf7115b3613291d84b441
push id89692
push usercsabou@mozilla.com
push dateThu, 11 Jul 2019 01:38:31 +0000
treeherderautoland@b810038f5eb1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1555483
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1555483 - Part 1: Add SVG filter primitive display item. r=gw Differential Revision: https://phabricator.services.mozilla.com/D34087
gfx/wr/examples/animation.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/scene.rs
gfx/wr/webrender/src/tiling.rs
gfx/wr/webrender_api/src/display_item.rs
gfx/wr/webrender_api/src/display_list.rs
gfx/wr/wrench/src/yaml_frame_reader.rs
gfx/wr/wrench/src/yaml_frame_writer.rs
gfx/wr/wrench/src/yaml_helper.rs
--- a/gfx/wr/examples/animation.rs
+++ b/gfx/wr/examples/animation.rs
@@ -66,16 +66,17 @@ impl App {
         );
 
         builder.push_simple_stacking_context_with_filters(
             LayoutPoint::zero(),
             spatial_id,
             true,
             &filters,
             &[],
+            &[]
         );
 
         let space_and_clip = SpaceAndClipInfo {
             spatial_id,
             clip_id: ClipId::root(pipeline_id),
         };
         let clip_bounds = LayoutRect::new(LayoutPoint::zero(), bounds.size);
         let complex_clip = ComplexClipRegion {
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -1,16 +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::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter};
 use api::{ClipId, ColorF, CommonItemProperties, ComplexClipRegion, RasterSpace};
 use api::{DisplayItem, DisplayItemRef, ExtendMode, ExternalScrollId};
-use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, GradientStop};
+use api::{FilterOp, FilterPrimitive, FontInstanceKey, GlyphInstance, GlyphOptions, GradientStop};
 use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, ColorDepth};
 use api::{LineOrientation, LineStyle, NinePatchBorderSource, PipelineId};
 use api::{PropertyBinding, ReferenceFrame, ReferenceFrameKind, ScrollFrameDisplayItem, ScrollSensitivity};
 use api::{Shadow, SpaceAndClipInfo, SpatialId, StackingContext, StickyFrameDisplayItem};
 use api::{ClipMode, PrimitiveKeyKind, TransformStyle, YuvColorSpace, YuvData, TempFilterData};
 use api::units::*;
 use crate::clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
@@ -791,29 +791,31 @@ impl<'a> DisplayListFlattener<'a> {
         &mut self,
         traversal: &mut BuiltDisplayListIter<'a>,
         pipeline_id: PipelineId,
         stacking_context: &StackingContext,
         spatial_node_index: SpatialNodeIndex,
         origin: LayoutPoint,
         filters: ItemRange<FilterOp>,
         filter_datas: &[TempFilterData],
+        filter_primitives: ItemRange<FilterPrimitive>,
         is_backface_visible: bool,
         apply_pipeline_clip: bool,
     ) {
         // Avoid doing unnecessary work for empty stacking contexts.
         if traversal.current_stacking_context_empty() {
             traversal.skip_current_stacking_context();
             return;
         }
 
         let composition_operations = {
             CompositeOps::new(
                 stacking_context.filter_ops_for_compositing(filters),
                 stacking_context.filter_datas_for_compositing(filter_datas),
+                stacking_context.filter_primitives_for_compositing(filter_primitives),
                 stacking_context.mix_blend_mode_for_compositing(),
             )
         };
 
         let clip_chain_id = match stacking_context.clip_id {
             Some(clip_id) => self.id_to_index_mapper.get_clip_chain_id(clip_id),
             None => ClipChainId::NONE,
         };
@@ -1175,16 +1177,17 @@ impl<'a> DisplayListFlattener<'a> {
                 self.flatten_stacking_context(
                     &mut subtraversal,
                     pipeline_id,
                     &info.stacking_context,
                     space,
                     info.origin,
                     item.filters(),
                     item.filter_datas(),
+                    item.filter_primitives(),
                     info.is_backface_visible,
                     apply_pipeline_clip,
                 );
                 return Some(subtraversal);
             }
             DisplayItem::PushReferenceFrame(ref info) => {
                 let parent_space = self.get_space(&info.parent_spatial_id);
                 let mut subtraversal = item.sub_iter();
@@ -1304,19 +1307,20 @@ impl<'a> DisplayListFlattener<'a> {
                 let parent_space = self.get_space(&info.parent_spatial_id);
                 self.flatten_sticky_frame(
                     info,
                     parent_space,
                 );
             }
 
             // Do nothing; these are dummy items for the display list parser
-            DisplayItem::SetGradientStops => {}
-            DisplayItem::SetFilterOps => {}
-            DisplayItem::SetFilterData => {}
+            DisplayItem::SetGradientStops |
+            DisplayItem::SetFilterOps |
+            DisplayItem::SetFilterData |
+            DisplayItem::SetFilterPrimitives => {}
 
             DisplayItem::PopReferenceFrame |
             DisplayItem::PopStackingContext => {
                 unreachable!("Should have returned in parent method.")
             }
             DisplayItem::PushShadow(info) => {
                 let clip_and_scroll = self.get_clip_and_scroll(
                     &info.space_and_clip.clip_id,
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.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::{BuiltDisplayList, ColorF, DynamicProperties, Epoch};
-use api::{FilterOp, TempFilterData, FilterData, ComponentTransferFuncType};
+use api::{FilterOp, TempFilterData, FilterData, FilterPrimitive, ComponentTransferFuncType};
 use api::{PipelineId, PropertyBinding, PropertyBindingId, ItemRange, MixBlendMode, StackingContext};
 use api::units::{LayoutSize, LayoutTransform};
 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.
@@ -201,16 +201,20 @@ pub trait StackingContextHelpers {
     fn filter_ops_for_compositing(
         &self,
         input_filters: ItemRange<FilterOp>,
     ) -> Vec<Filter>;
     fn filter_datas_for_compositing(
         &self,
         input_filter_datas: &[TempFilterData],
     ) -> Vec<FilterData>;
+    fn filter_primitives_for_compositing(
+        &self,
+        input_filter_primitives: ItemRange<FilterPrimitive>,
+    ) -> Vec<FilterPrimitive>;
 }
 
 impl StackingContextHelpers for StackingContext {
     fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode> {
         match self.mix_blend_mode {
             MixBlendMode::Normal => None,
             _ => Some(self.mix_blend_mode),
         }
@@ -249,9 +253,20 @@ impl StackingContextHelpers for Stacking
                 func_b_type: func_types[2],
                 b_values: temp_filter_data.b_values.iter().collect(),
                 func_a_type: func_types[3],
                 a_values: temp_filter_data.a_values.iter().collect(),
             });
         }
         filter_datas
     }
+
+    fn filter_primitives_for_compositing(
+        &self,
+        input_filter_primitives: ItemRange<FilterPrimitive>,
+    ) -> Vec<FilterPrimitive> {
+        // Resolve these in the flattener?
+        // TODO(gw): Now that we resolve these later on,
+        //           we could probably make it a bit
+        //           more efficient than cloning these here.
+        input_filter_primitives.iter().map(|primitive| primitive.into()).collect()
+    }
 }
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.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::{ColorF, BorderStyle, MixBlendMode, PipelineId, PremultipliedColorF};
-use api::{DocumentLayer, FilterData, ImageFormat, LineOrientation};
+use api::{DocumentLayer, FilterData, FilterPrimitive, 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, ROOT_SPATIAL_NODE_INDEX};
 use crate::debug_render::DebugItem;
 use crate::device::{Texture};
@@ -1327,28 +1327,33 @@ impl RenderPass {
     }
 }
 
 #[derive(Debug, Clone, Default)]
 pub struct CompositeOps {
     // Requires only a single texture as input (e.g. most filters)
     pub filters: Vec<Filter>,
     pub filter_datas: Vec<FilterData>,
+    pub filter_primitives: Vec<FilterPrimitive>,
 
     // Requires two source textures (e.g. mix-blend-mode)
     pub mix_blend_mode: Option<MixBlendMode>,
 }
 
 impl CompositeOps {
-    pub fn new(filters: Vec<Filter>,
-               filter_datas: Vec<FilterData>,
-               mix_blend_mode: Option<MixBlendMode>) -> Self {
+    pub fn new(
+        filters: Vec<Filter>,
+        filter_datas: Vec<FilterData>,
+        filter_primitives: Vec<FilterPrimitive>,
+        mix_blend_mode: Option<MixBlendMode>
+    ) -> Self {
         CompositeOps {
             filters,
             filter_datas,
+            filter_primitives,
             mix_blend_mode,
         }
     }
 
     pub fn is_empty(&self) -> bool {
         self.filters.is_empty() && self.filter_datas.is_empty() && self.mix_blend_mode.is_none()
     }
 }
--- a/gfx/wr/webrender_api/src/display_item.rs
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -116,16 +116,17 @@ pub enum DisplayItem {
     PushReferenceFrame(ReferenceFrameDisplayListItem),
     PushStackingContext(PushStackingContextDisplayItem),
 
     // These marker items indicate an array of data follows, to be used for the
     // next non-marker item.
     SetGradientStops,
     SetFilterOps,
     SetFilterData,
+    SetFilterPrimitives,
 
     // These marker items terminate a scope introduced by a previous item.
     PopReferenceFrame,
     PopStackingContext,
     PopAllShadows,
 }
 
 /// This is a "complete" version of the DisplayItem, with all implicit trailing
@@ -154,16 +155,17 @@ pub enum DebugDisplayItem {
     StickyFrame(StickyFrameDisplayItem),
     Iframe(IframeDisplayItem),
     PushReferenceFrame(ReferenceFrameDisplayListItem),
     PushStackingContext(PushStackingContextDisplayItem),
 
     SetGradientStops(Vec<GradientStop>),
     SetFilterOps(Vec<FilterOp>),
     SetFilterData(FilterData),
+    SetFilterPrimitives(Vec<FilterPrimitive>),
 
     PopReferenceFrame,
     PopStackingContext,
     PopAllShadows,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct ClipDisplayItem {
@@ -633,17 +635,17 @@ pub struct PushStackingContextDisplayIte
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct StackingContext {
     pub transform_style: TransformStyle,
     pub mix_blend_mode: MixBlendMode,
     pub clip_id: Option<ClipId>,
     pub raster_space: RasterSpace,
     /// True if picture caching should be used on this stacking context.
     pub cache_tiles: bool,
-} // IMPLICIT: filters: Vec<FilterOp>, filter_datas: Vec<FilterData>
+} // IMPLICIT: filters: Vec<FilterOp>, filter_datas: Vec<FilterData>, filter_primitives: Vec<FilterPrimitive>
 
 #[repr(u8)]
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
 pub enum TransformStyle {
     Flat = 0,
     Preserve3D = 1,
 }
 
@@ -692,16 +694,44 @@ pub enum MixBlendMode {
     Difference = 10,
     Exclusion = 11,
     Hue = 12,
     Saturation = 13,
     Color = 14,
     Luminosity = 15,
 }
 
+/// An input to a SVG filter primitive.
+#[repr(C)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+pub enum FilterPrimitiveInput {
+    /// The input is the original graphic that the filter is being applied to.
+    Original,
+    /// The input is the output of the previous filter primitive in the filter primitive chain.
+    Previous,
+    /// The input is the output of the filter primitive at the given index in the filter primitive chain.
+    OutputOfPrimitiveIndex(usize),
+}
+
+#[repr(C)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+pub struct BlendPrimitive {
+    pub input1: FilterPrimitiveInput,
+    pub input2: FilterPrimitiveInput,
+    pub mode: MixBlendMode,
+}
+
+/// SVG Filter Primitive.
+#[repr(C)]
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+pub enum FilterPrimitive {
+    Blend(BlendPrimitive),
+}
+
+/// CSS filter.
 #[repr(C)]
 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
 pub enum FilterOp {
     /// Filter that does no transformation of the colors, needed for
     /// debug purposes only.
     Identity,
     Blur(f32),
     Brightness(f32),
@@ -1157,16 +1187,17 @@ impl DisplayItem {
             DisplayItem::PopAllShadows => "pop_all_shadows",
             DisplayItem::PopReferenceFrame => "pop_reference_frame",
             DisplayItem::PopStackingContext => "pop_stacking_context",
             DisplayItem::PushShadow(..) => "push_shadow",
             DisplayItem::PushReferenceFrame(..) => "push_reference_frame",
             DisplayItem::PushStackingContext(..) => "push_stacking_context",
             DisplayItem::SetFilterOps => "set_filter_ops",
             DisplayItem::SetFilterData => "set_filter_data",
+            DisplayItem::SetFilterPrimitives => "set_filter_primitives",
             DisplayItem::RadialGradient(..) => "radial_gradient",
             DisplayItem::Rectangle(..) => "rectangle",
             DisplayItem::ScrollFrame(..) => "scroll_frame",
             DisplayItem::SetGradientStops => "set_gradient_stops",
             DisplayItem::StickyFrame(..) => "sticky_frame",
             DisplayItem::Text(..) => "text",
             DisplayItem::YuvImage(..) => "yuv_image",
         }
--- a/gfx/wr/webrender_api/src/display_list.rs
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -126,16 +126,17 @@ pub struct BuiltDisplayListDescriptor {
 pub struct BuiltDisplayListIter<'a> {
     list: &'a BuiltDisplayList,
     data: &'a [u8],
     cur_item: di::DisplayItem,
     cur_stops: ItemRange<'a, di::GradientStop>,
     cur_glyphs: ItemRange<'a, GlyphInstance>,
     cur_filters: ItemRange<'a, di::FilterOp>,
     cur_filter_data: Vec<TempFilterData<'a>>,
+    cur_filter_primitives: ItemRange<'a, di::FilterPrimitive>,
     cur_clip_chain_items: ItemRange<'a, di::ClipId>,
     cur_complex_clip: ItemRange<'a, di::ComplexClipRegion>,
     peeking: Peek,
     /// Should just be initialized but never populated in release builds
     debug_stats: DebugStats,
 }
 
 /// Internal info used for more detailed analysis of serialized display lists
@@ -292,16 +293,17 @@ impl<'a> BuiltDisplayListIter<'a> {
         BuiltDisplayListIter {
             list,
             data,
             cur_item: di::DisplayItem::PopStackingContext,
             cur_stops: ItemRange::default(),
             cur_glyphs: ItemRange::default(),
             cur_filters: ItemRange::default(),
             cur_filter_data: Vec::new(),
+            cur_filter_primitives: ItemRange::default(),
             cur_clip_chain_items: ItemRange::default(),
             cur_complex_clip: ItemRange::default(),
             peeking: Peek::NotPeeking,
             debug_stats: DebugStats {
                 last_addr: data.as_ptr() as usize,
                 stats: HashMap::default(),
             }
         }
@@ -330,17 +332,18 @@ impl<'a> BuiltDisplayListIter<'a> {
         self.cur_complex_clip = ItemRange::default();
         self.cur_clip_chain_items = ItemRange::default();
 
         loop {
             self.next_raw()?;
             match self.cur_item {
                 SetGradientStops |
                 SetFilterOps |
-                SetFilterData => {
+                SetFilterData |
+                SetFilterPrimitives => {
                     // These are marker items for populating other display items, don't yield them.
                     continue;
                 }
                 _ => {
                     break;
                 }
             }
         }
@@ -386,16 +389,20 @@ impl<'a> BuiltDisplayListIter<'a> {
 
                 let data = *self.cur_filter_data.last().unwrap();
                 self.debug_stats.log_slice("set_filter_data.func_types", &data.func_types);
                 self.debug_stats.log_slice("set_filter_data.r_values", &data.r_values);
                 self.debug_stats.log_slice("set_filter_data.g_values", &data.g_values);
                 self.debug_stats.log_slice("set_filter_data.b_values", &data.b_values);
                 self.debug_stats.log_slice("set_filter_data.a_values", &data.a_values);
             }
+            SetFilterPrimitives => {
+                self.cur_filter_primitives = skip_slice::<di::FilterPrimitive>(&mut self.data);
+                self.debug_stats.log_slice("set_filter_primitives.primitives", &self.cur_filter_primitives);
+            }
             ClipChain(_) => {
                 self.cur_clip_chain_items = skip_slice::<di::ClipId>(&mut self.data);
                 self.debug_stats.log_slice("clip_chain.clip_ids", &self.cur_clip_chain_items);
             }
             Clip(_) | ScrollFrame(_) => {
                 self.cur_complex_clip = skip_slice::<di::ComplexClipRegion>(&mut self.data);
                 let name = if let Clip(_) = self.cur_item {
                     "clip.complex_clips"
@@ -497,16 +504,20 @@ impl<'a, 'b> DisplayItemRef<'a, 'b> {
     pub fn filters(&self) -> ItemRange<di::FilterOp> {
         self.iter.cur_filters
     }
 
     pub fn filter_datas(&self) -> &Vec<TempFilterData> {
         &self.iter.cur_filter_data
     }
 
+    pub fn filter_primitives(&self) -> ItemRange<di::FilterPrimitive> {
+        self.iter.cur_filter_primitives
+    }
+
     pub fn clip_chain_items(&self) -> ItemRange<di::ClipId> {
         self.iter.cur_clip_chain_items
     }
 
     pub fn display_list(&self) -> &BuiltDisplayList {
         self.iter.display_list()
     }
 
@@ -597,16 +608,19 @@ impl Serialize for BuiltDisplayList {
                         func_g_type: func_types[1],
                         g_values: temp_filter_data.g_values.iter().collect(),
                         func_b_type: func_types[2],
                         b_values: temp_filter_data.b_values.iter().collect(),
                         func_a_type: func_types[3],
                         a_values: temp_filter_data.a_values.iter().collect(),
                     })
                 },
+                Real::SetFilterPrimitives => Debug::SetFilterPrimitives(
+                    item.iter.cur_filter_primitives.iter().collect()
+                ),
                 Real::SetGradientStops => Debug::SetGradientStops(
                     item.iter.cur_stops.iter().collect()
                 ),
                 Real::StickyFrame(v) => Debug::StickyFrame(v),
                 Real::Rectangle(v) => Debug::Rectangle(v),
                 Real::ClearRectangle(v) => Debug::ClearRectangle(v),
                 Real::HitTest(v) => Debug::HitTest(v),
                 Real::Line(v) => Debug::Line(v),
@@ -695,16 +709,20 @@ impl<'de> Deserialize<'de> for BuiltDisp
                          filter_data.func_a_type].to_vec();
                     DisplayListBuilder::push_iter_impl(&mut temp, func_types);
                     DisplayListBuilder::push_iter_impl(&mut temp, filter_data.r_values);
                     DisplayListBuilder::push_iter_impl(&mut temp, filter_data.g_values);
                     DisplayListBuilder::push_iter_impl(&mut temp, filter_data.b_values);
                     DisplayListBuilder::push_iter_impl(&mut temp, filter_data.a_values);
                     Real::SetFilterData
                 },
+                Debug::SetFilterPrimitives(filter_primitives) => {
+                    DisplayListBuilder::push_iter_impl(&mut temp, filter_primitives);
+                    Real::SetFilterPrimitives
+                }
                 Debug::SetGradientStops(stops) => {
                     DisplayListBuilder::push_iter_impl(&mut temp, stops);
                     Real::SetGradientStops
                 },
 
                 Debug::Rectangle(v) => Real::Rectangle(v),
                 Debug::ClearRectangle(v) => Real::ClearRectangle(v),
                 Debug::HitTest(v) => Real::HitTest(v),
@@ -1420,16 +1438,17 @@ impl DisplayListBuilder {
         origin: LayoutPoint,
         spatial_id: di::SpatialId,
         is_backface_visible: bool,
         clip_id: Option<di::ClipId>,
         transform_style: di::TransformStyle,
         mix_blend_mode: di::MixBlendMode,
         filters: &[di::FilterOp],
         filter_datas: &[di::FilterData],
+        filter_primitives: &[di::FilterPrimitive],
         raster_space: di::RasterSpace,
         cache_tiles: bool,
     ) {
         if filters.len() > 0 {
             self.push_item(&di::DisplayItem::SetFilterOps);
             self.push_iter(filters);
         }
 
@@ -1440,16 +1459,21 @@ impl DisplayListBuilder {
             self.push_item(&di::DisplayItem::SetFilterData);
             self.push_iter(&func_types);
             self.push_iter(&filter_data.r_values);
             self.push_iter(&filter_data.g_values);
             self.push_iter(&filter_data.b_values);
             self.push_iter(&filter_data.a_values);
         }
 
+        if !filter_primitives.is_empty() {
+            self.push_item(&di::DisplayItem::SetFilterPrimitives);
+            self.push_iter(filter_primitives);
+        }
+
         let item = di::DisplayItem::PushStackingContext(di::PushStackingContextDisplayItem {
             origin,
             spatial_id,
             is_backface_visible,
             stacking_context: di::StackingContext {
                 transform_style,
                 mix_blend_mode,
                 clip_id,
@@ -1463,37 +1487,39 @@ impl DisplayListBuilder {
 
     /// Helper for examples/ code.
     pub fn push_simple_stacking_context(
         &mut self,
         origin: LayoutPoint,
         spatial_id: di::SpatialId,
         is_backface_visible: bool,
     ) {
-        self.push_simple_stacking_context_with_filters(origin, spatial_id, is_backface_visible, &[], &[]);
+        self.push_simple_stacking_context_with_filters(origin, spatial_id, is_backface_visible, &[], &[], &[]);
     }
 
     /// Helper for examples/ code.
     pub fn push_simple_stacking_context_with_filters(
         &mut self,
         origin: LayoutPoint,
         spatial_id: di::SpatialId,
         is_backface_visible: bool,
         filters: &[di::FilterOp],
         filter_datas: &[di::FilterData],
+        filter_primitives: &[di::FilterPrimitive],
     ) {
         self.push_stacking_context(
             origin,
             spatial_id,
             is_backface_visible,
             None,
             di::TransformStyle::Flat,
             di::MixBlendMode::Normal,
             filters,
             filter_datas,
+            filter_primitives,
             di::RasterSpace::Screen,
             /* cache_tiles = */ false,
         );
     }
 
     pub fn pop_stacking_context(&mut self) {
         self.push_item(&di::DisplayItem::PopStackingContext);
     }
--- a/gfx/wr/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wr/wrench/src/yaml_frame_reader.rs
@@ -1876,26 +1876,28 @@ impl YamlFrameReader {
             if let Some(size) = yaml["scroll-offset"].as_point() {
                 let external_id = ExternalScrollId(0, dl.pipeline_id);
                 self.scroll_offsets.insert(external_id, LayoutPoint::new(size.x, size.y));
             }
         }
 
         let filters = yaml["filters"].as_vec_filter_op().unwrap_or(vec![]);
         let filter_datas = yaml["filter-datas"].as_vec_filter_data().unwrap_or(vec![]);
+        let filter_primitives = yaml["filter-primitives"].as_vec_filter_primitive().unwrap_or(vec![]);
 
         dl.push_stacking_context(
             bounds.origin,
             *self.spatial_id_stack.last().unwrap(),
             info.is_backface_visible,
             clip_node_id,
             transform_style,
             mix_blend_mode,
             &filters,
             &filter_datas,
+            &filter_primitives,
             raster_space,
             cache_tiles,
         );
 
         if !yaml["items"].is_badvalue() {
             self.add_display_list_items_from_yaml(dl, wrench, &yaml["items"]);
         }
 
--- a/gfx/wr/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wr/wrench/src/yaml_frame_writer.rs
@@ -67,16 +67,24 @@ fn color_to_string(value: ColorF) -> Str
             value.r * 255.0,
             value.g * 255.0,
             value.b * 255.0,
             value.a
         )
     }
 }
 
+fn filter_input_to_string(input: FilterPrimitiveInput) -> String {
+    match input {
+        FilterPrimitiveInput::Original => "original".into(),
+        FilterPrimitiveInput::Previous => "previous".into(),
+        FilterPrimitiveInput::OutputOfPrimitiveIndex(index) => index.to_string(),
+    }
+}
+
 fn color_node(parent: &mut Table, key: &str, value: ColorF) {
     yaml_node(parent, key, Yaml::String(color_to_string(value)));
 }
 
 fn point_node<U>(parent: &mut Table, key: &str, value: &TypedPoint2D<f32, U>) {
     f32_vec_node(parent, key, &[value.x, value.y]);
 }
 
@@ -248,16 +256,17 @@ fn shadow_parameters(shadow: &Shadow) ->
 }
 
 fn write_stacking_context(
     parent: &mut Table,
     sc: &StackingContext,
     properties: &SceneProperties,
     filter_iter: impl IntoIterator<Item = FilterOp>,
     filter_data_iter: &[TempFilterData],
+    filter_primitive_iter: impl IntoIterator<Item = FilterPrimitive>,
 ) {
     enum_node(parent, "transform-style", sc.transform_style);
 
     let raster_space = match sc.raster_space {
         RasterSpace::Local(scale) => {
             format!("local({})", scale)
         }
         RasterSpace::Screen => {
@@ -344,16 +353,33 @@ fn write_stacking_context(
             Yaml::Array(g_values),
             Yaml::Array(b_values),
             Yaml::Array(a_values),
         ].to_vec();
         filter_datas.push(Yaml::Array(avec));
     }
 
     yaml_node(parent, "filter-datas", Yaml::Array(filter_datas));
+
+    // filter primitives
+    let mut filter_primitives = vec![];
+    for filter_primitive in filter_primitive_iter {
+        let mut table = new_table();
+        match filter_primitive {
+            FilterPrimitive::Blend(blend_primitive) => {
+                yaml_node(&mut table, "type", Yaml::String("blend".into()));
+                yaml_node(&mut table, "in1", Yaml::String(filter_input_to_string(blend_primitive.input1)));
+                yaml_node(&mut table, "in2", Yaml::String(filter_input_to_string(blend_primitive.input2)));
+                enum_node(&mut table, "mode", blend_primitive.mode);
+            }
+        }
+        filter_primitives.push(Yaml::Hash(table));
+    }
+
+    yaml_node(parent, "filter-primitives", Yaml::Array(filter_primitives));
 }
 
 #[cfg(target_os = "macos")]
 fn native_font_handle_to_yaml(
     rsrc: &mut ResourceGenerator,
     handle: &NativeFontHandle,
     parent: &mut yaml_rust::yaml::Hash,
     path_opt: &mut Option<PathBuf>,
@@ -1160,16 +1186,17 @@ impl YamlFrameWriter {
                     point_node(&mut v, "origin", &item.origin);
                     bool_node(&mut v, "backface-visible", item.is_backface_visible);
                     write_stacking_context(
                         &mut v,
                         &item.stacking_context,
                         &scene.properties,
                         base.filters(),
                         base.filter_datas(),
+                        base.filter_primitives(),
                     );
 
                     let mut sub_iter = base.sub_iter();
                     self.write_display_list(&mut v, display_list, scene, &mut sub_iter, clip_id_mapper);
                     continue_traversal = Some(sub_iter);
                 }
                 DisplayItem::PushReferenceFrame(item) => {
                     str_node(&mut v, "type", "reference-frame");
@@ -1272,19 +1299,21 @@ impl YamlFrameWriter {
                         Yaml::Real(item.previously_applied_offset.y.to_string()),
                     ];
                     yaml_node(&mut v, "previously-applied-offset", Yaml::Array(applied));
                 }
 
                 DisplayItem::PopReferenceFrame |
                 DisplayItem::PopStackingContext => return,
 
-                DisplayItem::SetGradientStops => panic!("dummy item yielded?"),
-                DisplayItem::SetFilterOps => panic!("dummy item yielded?"),
-                DisplayItem::SetFilterData => panic!("dummy item yielded?"),
+                DisplayItem::SetGradientStops |
+                DisplayItem::SetFilterOps |
+                DisplayItem::SetFilterData |
+                DisplayItem::SetFilterPrimitives => panic!("dummy item yielded?"),
+
                 DisplayItem::PushShadow(item) => {
                     str_node(&mut v, "type", "shadow");
                     vector_node(&mut v, "offset", &item.shadow.offset);
                     color_node(&mut v, "color", item.shadow.color);
                     f32_node(&mut v, "blur-radius", item.shadow.blur_radius);
                 }
                 DisplayItem::PopAllShadows => {
                     str_node(&mut v, "type", "pop-all-shadows");
--- a/gfx/wr/wrench/src/yaml_helper.rs
+++ b/gfx/wr/wrench/src/yaml_helper.rs
@@ -33,16 +33,19 @@ pub trait YamlHelper {
     fn as_transform_style(&self) -> Option<TransformStyle>;
     fn as_raster_space(&self) -> Option<RasterSpace>;
     fn as_clip_mode(&self) -> Option<ClipMode>;
     fn as_mix_blend_mode(&self) -> Option<MixBlendMode>;
     fn as_filter_op(&self) -> Option<FilterOp>;
     fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>>;
     fn as_filter_data(&self) -> Option<FilterData>;
     fn as_vec_filter_data(&self) -> Option<Vec<FilterData>>;
+    fn as_filter_input(&self) -> Option<FilterPrimitiveInput>;
+    fn as_filter_primitive(&self) -> Option<FilterPrimitive>;
+    fn as_vec_filter_primitive(&self) -> Option<Vec<FilterPrimitive>>;
 }
 
 fn string_to_color(color: &str) -> Option<ColorF> {
     match color {
         "red" => Some(ColorF::new(1.0, 0.0, 0.0, 1.0)),
         "green" => Some(ColorF::new(0.0, 1.0, 0.0, 1.0)),
         "blue" => Some(ColorF::new(0.0, 0.0, 1.0, 1.0)),
         "white" => Some(ColorF::new(1.0, 1.0, 1.0, 1.0)),
@@ -669,16 +672,59 @@ impl YamlHelper for Yaml {
                         }
                     }
                 }
             }
         }
         None
     }
 
+    fn as_filter_input(&self) -> Option<FilterPrimitiveInput> {
+        if let Some(input) = self.as_str() {
+            match input {
+                "original" => Some(FilterPrimitiveInput::Original),
+                "previous" => Some(FilterPrimitiveInput::Previous),
+                _ => None,
+            }
+        } else if let Some(index) = self.as_i64() {
+            if index >= 0 {
+                Some(FilterPrimitiveInput::OutputOfPrimitiveIndex(index as usize))
+            } else {
+                panic!("Filter input index cannot be negative");
+            }
+        } else {
+            panic!("Invalid filter input");
+        }
+    }
+
     fn as_vec_filter_data(&self) -> Option<Vec<FilterData>> {
         if let Some(v) = self.as_vec() {
             Some(v.iter().map(|x| x.as_filter_data().unwrap()).collect())
         } else {
             self.as_filter_data().map(|data| vec![data])
         }
     }
+
+    fn as_filter_primitive(&self) -> Option<FilterPrimitive> {
+        if let Some(filter_type) = self["type"].as_str() {
+            match filter_type {
+                "blend" => {
+                    Some(FilterPrimitive::Blend(BlendPrimitive {
+                        input1: self["in1"].as_filter_input().unwrap(),
+                        input2: self["in2"].as_filter_input().unwrap(),
+                        mode: self["blend-mode"].as_mix_blend_mode().unwrap(),
+                    }))
+                }
+                _ => None,
+            }
+        } else {
+            None
+        }
+    }
+
+    fn as_vec_filter_primitive(&self) -> Option<Vec<FilterPrimitive>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|x| x.as_filter_primitive().unwrap()).collect())
+        } else {
+            self.as_filter_primitive().map(|data| vec![data])
+        }
+    }
 }