Bug 1490282. Update webrender to commit 02f14d0f333ef125d1abff7b1146039a0ba75f43
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Wed, 12 Sep 2018 00:09:43 -0400
changeset 436023 cd04aed8011fee8cb3c59446c72def78e5a98e2b
parent 436022 2a3f3c12297671583c0ce5e2494d9686c2bcd995
child 436024 7505dc56eead8f654f19be80a0aa2b7773a05999
push id34625
push userdvarga@mozilla.com
push dateThu, 13 Sep 2018 02:31:40 +0000
treeherdermozilla-central@51e9e9660b3e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1490282
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1490282. Update webrender to commit 02f14d0f333ef125d1abff7b1146039a0ba75f43
gfx/webrender/res/cs_border_segment.glsl
gfx/webrender/src/border.rs
gfx/webrender/src/clip.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/renderer.rs
gfx/webrender_api/src/api.rs
gfx/webrender_api/src/channel.rs
gfx/webrender_bindings/revision.txt
--- a/gfx/webrender/res/cs_border_segment.glsl
+++ b/gfx/webrender/res/cs_border_segment.glsl
@@ -56,19 +56,20 @@ varying vec2 vPos;
 #define BORDER_STYLE_DOTTED       3
 #define BORDER_STYLE_DASHED       4
 #define BORDER_STYLE_HIDDEN       5
 #define BORDER_STYLE_GROOVE       6
 #define BORDER_STYLE_RIDGE        7
 #define BORDER_STYLE_INSET        8
 #define BORDER_STYLE_OUTSET       9
 
-#define CLIP_NONE   0
-#define CLIP_DASH   1
-#define CLIP_DOT    2
+#define CLIP_NONE        0
+#define CLIP_DASH_CORNER 1
+#define CLIP_DASH_EDGE   2
+#define CLIP_DOT         3
 
 #ifdef WR_VERTEX_SHADER
 
 in vec2 aTaskOrigin;
 in vec4 aRect;
 in vec4 aColor0;
 in vec4 aColor1;
 in int aFlags;
@@ -351,17 +352,31 @@ void main(void) {
     float d = -1.0;
 
     switch (clip_mode) {
         case CLIP_DOT: {
             // Set clip distance based or dot position and radius.
             d = distance(vClipParams1.xy, vPos) - vClipParams1.z;
             break;
         }
-        case CLIP_DASH: {
+        case CLIP_DASH_EDGE: {
+            bool is_vertical = vClipParams1.x == 0.;
+            float half_dash = is_vertical ? vClipParams1.y : vClipParams1.x;
+            // We want to draw something like:
+            // +---+---+---+---+
+            // |xxx|   |   |xxx|
+            // +---+---+---+---+
+            float pos = is_vertical ? vPos.y : vPos.x;
+            bool in_dash = pos < half_dash || pos > 3.0 * half_dash;
+            if (!in_dash) {
+                d = 1.;
+            }
+            break;
+        }
+        case CLIP_DASH_CORNER: {
             // Get SDF for the two line/tangent clip lines,
             // do SDF subtract to get clip distance.
             float d0 = distance_to_line(vClipParams1.xy,
                                         vClipParams1.zw,
                                         vPos);
             float d1 = distance_to_line(vClipParams2.xy,
                                         vClipParams2.zw,
                                         vPos);
--- a/gfx/webrender/src/border.rs
+++ b/gfx/webrender/src/border.rs
@@ -230,64 +230,64 @@ impl BorderSideHelpers for BorderSide {
         let black = if lighter { 0.7 } else { 0.3 };
         ColorF::new(black, black, black, self.color.a)
     }
 }
 
 /// The kind of border corner clip.
 #[repr(C)]
 #[derive(Copy, Debug, Clone, PartialEq)]
-pub enum BorderCornerClipKind {
-    Dash = 1,
-    Dot = 2,
+pub enum BorderClipKind {
+    DashCorner = 1,
+    DashEdge = 2,
+    Dot = 3,
 }
 
 /// The source data for a border corner clip mask.
 #[derive(Debug, Clone)]
-pub struct BorderCornerClipSource {
-    pub max_clip_count: usize,
-    kind: BorderCornerClipKind,
+struct BorderCornerClipSource {
+    // FIXME(emilio): the `max_clip_count` name makes no sense for dashed
+    // borders now that it represents half-dashes.
+    max_clip_count: usize,
+    kind: BorderClipKind,
     widths: DeviceSize,
     radius: DeviceSize,
     ellipse: Ellipse<DevicePixel>,
 }
 
 impl BorderCornerClipSource {
     pub fn new(
         corner_radius: DeviceSize,
         widths: DeviceSize,
-        kind: BorderCornerClipKind,
+        kind: BorderClipKind,
     ) -> BorderCornerClipSource {
         // Work out a dash length (and therefore dash count)
         // based on the width of the border edges. The "correct"
         // dash length is not mentioned in the CSS borders
         // spec. The calculation below is similar, but not exactly
         // the same as what Gecko uses.
         // TODO(gw): Iterate on this to get it closer to what Gecko
         //           uses for dash length.
 
         let (ellipse, max_clip_count) = match kind {
-            BorderCornerClipKind::Dash => {
+            BorderClipKind::DashEdge => unreachable!("not for corners"),
+            BorderClipKind::DashCorner => {
                 let ellipse = Ellipse::new(corner_radius);
 
-                // The desired dash length is ~3x the border width.
                 let average_border_width = 0.5 * (widths.width + widths.height);
-                let desired_dash_arc_length = average_border_width * 3.0;
 
-                // Get the ideal number of dashes for that arc length.
-                // This is scaled by 0.5 since there is an on/off length
-                // for each dash.
-                let desired_count = 0.5 * ellipse.total_arc_length / desired_dash_arc_length;
+                let (_half_dash, num_half_dashes) =
+                    compute_half_dash(average_border_width, ellipse.total_arc_length);
 
                 // Round that up to the nearest integer, so that the dash length
                 // doesn't exceed the ratio above. Add one extra dash to cover
                 // the last half-dash of the arc.
-                (ellipse, desired_count.ceil() as usize)
+                (ellipse, num_half_dashes as usize)
             }
-            BorderCornerClipKind::Dot => {
+            BorderClipKind::Dot => {
                 let mut corner_radius = corner_radius;
                 if corner_radius.width < (widths.width / 2.0) {
                     corner_radius.width = 0.0;
                 }
                 if corner_radius.height < (widths.height / 2.0) {
                     corner_radius.height = 0.0;
                 }
 
@@ -329,16 +329,20 @@ impl BorderCornerClipSource {
     //           for now in order to reduce the size of the
     //           patch a bit. In the future, when we spent some
     //           time working on dot/dash placement, we should
     //           restructure this code to be more consistent
     //           with how border rendering works now.
     pub fn write(self, segment: BorderSegment) -> Vec<[f32; 8]> {
         let mut dot_dash_data = Vec::new();
 
+        if self.max_clip_count == 0 {
+            return dot_dash_data;
+        }
+
         let outer_scale = match segment {
             BorderSegment::TopLeft => DeviceVector2D::new(0.0, 0.0),
             BorderSegment::TopRight => DeviceVector2D::new(1.0, 0.0),
             BorderSegment::BottomRight => DeviceVector2D::new(1.0, 1.0),
             BorderSegment::BottomLeft => DeviceVector2D::new(0.0, 1.0),
             _ => unreachable!(),
         };
         let outer = DevicePoint::new(
@@ -348,39 +352,42 @@ impl BorderCornerClipSource {
         let clip_sign = DeviceVector2D::new(
             1.0 - 2.0 * outer_scale.x,
             1.0 - 2.0 * outer_scale.y,
         );
 
         let max_clip_count = self.max_clip_count.min(MAX_DASH_COUNT);
 
         match self.kind {
-            BorderCornerClipKind::Dash => {
-                // Get the correct dash arc length.
-                let dash_arc_length =
-                    0.5 * self.ellipse.total_arc_length / max_clip_count as f32;
-                // Start the first dash at one quarter the length of a single dash
-                // along the arc line. This is arbitrary but looks reasonable in
-                // most cases. We need to spend some time working on a more
-                // sophisticated dash placement algorithm that takes into account
-                // the offset of the dashes along edge segments.
-                let mut current_arc_length = 0.25 * dash_arc_length;
-                dot_dash_data.reserve(max_clip_count);
-                for _ in 0 .. max_clip_count {
-                    let arc_length0 = current_arc_length;
-                    current_arc_length += dash_arc_length;
+            BorderClipKind::DashEdge => unreachable!("not for corners"),
+            BorderClipKind::DashCorner => {
+                // Get the correct half-dash arc length.
+                let half_dash_arc_length =
+                    self.ellipse.total_arc_length / max_clip_count as f32;
+                let dash_length = 2. * half_dash_arc_length;
+
+                let mut current_length = 0.;
 
-                    let arc_length1 = current_arc_length;
-                    current_arc_length += dash_arc_length;
+                dot_dash_data.reserve(max_clip_count / 4 + 1);
+                for i in 0 .. (max_clip_count / 4 + 1) {
+                    let arc_length0 = current_length;
+                    current_length += if i == 0 {
+                        half_dash_arc_length
+                    } else {
+                        dash_length
+                    };
+
+                    let arc_length1 = current_length;
+                    current_length += dash_length;
 
                     let alpha = self.ellipse.find_angle_for_arc_length(arc_length0);
-                    let beta =  self.ellipse.find_angle_for_arc_length(arc_length1);
+                    let beta = self.ellipse.find_angle_for_arc_length(arc_length1);
 
-                    let (point0, tangent0) =  self.ellipse.get_point_and_tangent(alpha);
-                    let (point1, tangent1) =  self.ellipse.get_point_and_tangent(beta);
+                    let (point0, tangent0) = self.ellipse.get_point_and_tangent(alpha);
+                    let (point1, tangent1) = self.ellipse.get_point_and_tangent(beta);
 
                     let point0 = DevicePoint::new(
                         outer.x + clip_sign.x * (self.radius.width - point0.x),
                         outer.y + clip_sign.y * (self.radius.height - point0.y),
                     );
 
                     let tangent0 = DeviceVector2D::new(
                         -tangent0.x * clip_sign.x,
@@ -404,24 +411,24 @@ impl BorderCornerClipSource {
                         tangent0.y,
                         point1.x,
                         point1.y,
                         tangent1.x,
                         tangent1.y,
                     ]);
                 }
             }
-            BorderCornerClipKind::Dot if max_clip_count == 1 => {
+            BorderClipKind::Dot if max_clip_count == 1 => {
                 let dot_diameter = lerp(self.widths.width, self.widths.height, 0.5);
                 dot_dash_data.push([
                     self.widths.width / 2.0, self.widths.height / 2.0, 0.5 * dot_diameter, 0.,
                     0., 0., 0., 0.,
                 ]);
             }
-            BorderCornerClipKind::Dot => {
+            BorderClipKind::Dot => {
                 let mut forward_dots = Vec::with_capacity(max_clip_count / 2 + 1);
                 let mut back_dots = Vec::with_capacity(max_clip_count / 2 + 1);
                 let mut leftover_arc_length = 0.0;
 
                 // Alternate between adding dots at the start and end of the
                 // ellipse arc. This ensures that we always end up with an exact
                 // half dot at each end of the arc, to match up with the edges.
                 forward_dots.push(DotInfo::new(self.widths.width, self.widths.width));
@@ -534,23 +541,24 @@ pub struct BorderSegmentInfo {
 }
 
 #[derive(Debug)]
 pub struct BorderRenderTaskInfo {
     pub border_segments: Vec<BorderSegmentInfo>,
     pub size: DeviceIntSize,
 }
 
-// Information needed to place and draw a border edge.
+/// Information needed to place and draw a border edge.
+#[derive(Debug)]
 struct EdgeInfo {
-    // Offset in local space to place the edge from origin.
+    /// Offset in local space to place the edge from origin.
     local_offset: f32,
-    // Size of the edge in local space.
+    /// Size of the edge in local space.
     local_size: f32,
-    // Size in device pixels needed in the render task.
+    /// Size in device pixels needed in the render task.
     device_size: f32,
 }
 
 impl EdgeInfo {
     fn new(
         local_offset: f32,
         local_size: f32,
         device_size: f32,
@@ -558,40 +566,61 @@ impl EdgeInfo {
         EdgeInfo {
             local_offset,
             local_size,
             device_size,
         }
     }
 }
 
+// Given a side width and the available space, compute the half-dash (half of
+// the 'on' segment) and the count of them for a given segment.
+fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) {
+    let half_dash = side_width * 1.5;
+    let num_half_dashes = (total_size / half_dash).ceil() as u32;
+
+    if num_half_dashes == 0 {
+        return (0., 0);
+    }
+
+    // TODO(emilio): Gecko has some other heuristics here to start with a full
+    // dash when the border side is zero, for example. We might consider those
+    // in the future.
+    let num_half_dashes = if num_half_dashes % 4 != 0 {
+        num_half_dashes + 4 - num_half_dashes % 4
+    } else {
+        num_half_dashes
+    };
+
+    let half_dash = total_size / num_half_dashes as f32;
+    (half_dash, num_half_dashes)
+}
+
+
 // Get the needed size in device pixels for an edge,
 // based on the border style of that edge. This is used
 // to determine how big the render task should be.
 fn get_edge_info(
     style: BorderStyle,
     side_width: f32,
     avail_size: f32,
     scale: f32,
 ) -> EdgeInfo {
     // To avoid division by zero below.
     if side_width <= 0.0 {
         return EdgeInfo::new(0.0, 0.0, 0.0);
     }
 
     match style {
         BorderStyle::Dashed => {
-            let dash_size = 3.0 * side_width;
-            let approx_dash_count = (avail_size - dash_size) / dash_size;
-            let dash_count = 1.0 + 2.0 * (approx_dash_count / 2.0).floor();
-            let used_size = dash_count * dash_size;
-            let extra_space = avail_size - used_size;
-            let device_size = 2.0 * dash_size * scale;
-            let offset = (extra_space * 0.5).round();
-            EdgeInfo::new(offset, used_size, device_size)
+            // Basically, two times the dash size.
+            let (half_dash, _num_half_dashes) =
+                compute_half_dash(side_width, avail_size);
+            let device_size = (2.0 * 2.0 * half_dash * scale).round();
+            EdgeInfo::new(0., avail_size, device_size)
         }
         BorderStyle::Dotted => {
             let dot_and_space_size = 2.0 * side_width;
             if avail_size < dot_and_space_size * 0.75 {
                 return EdgeInfo::new(0.0, 0.0, 0.0);
             }
             let approx_dot_count = avail_size / dot_and_space_size;
             let dot_count = approx_dot_count.floor().max(1.0);
@@ -955,17 +984,17 @@ fn add_brush_segment(
     task_rect: DeviceRect,
     brush_flags: BrushFlags,
     edge_flags: EdgeAaSegmentMask,
     brush_segments: &mut Vec<BrushSegment>,
 ) {
     brush_segments.push(
         BrushSegment::new(
             image_rect,
-            true,
+            /* may_need_clip_mask = */ true,
             edge_flags,
             [
                 task_rect.origin.x,
                 task_rect.origin.y,
                 task_rect.origin.x + task_rect.size.width,
                 task_rect.origin.y + task_rect.size.height,
             ],
             brush_flags,
@@ -1009,97 +1038,88 @@ fn add_segment(
             //           in the future by submitting two instances, each one with one side
             //           color set to have an alpha of 0.
             if (style0 == BorderStyle::Dotted && style1 == BorderStyle::Dashed) ||
                (style0 == BorderStyle::Dashed && style0 == BorderStyle::Dotted) {
                 warn!("TODO: Handle a corner with dotted / dashed transition.");
             }
 
             let clip_kind = match style0 {
-                BorderStyle::Dashed => Some(BorderCornerClipKind::Dash),
-                BorderStyle::Dotted => Some(BorderCornerClipKind::Dot),
+                BorderStyle::Dashed => Some(BorderClipKind::DashCorner),
+                BorderStyle::Dotted => Some(BorderClipKind::Dot),
                 _ => None,
             };
 
             match clip_kind {
                 Some(clip_kind) => {
                     let clip_source = BorderCornerClipSource::new(
                         radius,
                         widths,
                         clip_kind,
                     );
 
                     // TODO(gw): Restructure the BorderCornerClipSource code
                     //           so that we don't allocate a Vec here.
                     let clip_list = clip_source.write(segment);
 
-                    for params in clip_list {
-                        instances.push(BorderInstance {
-                            flags: base_flags | ((clip_kind as i32) << 24),
-                            clip_params: params,
-                            ..base_instance
-                        });
+                    if clip_list.is_empty() {
+                        instances.push(base_instance);
+                    } else {
+                        for params in clip_list {
+                            instances.push(BorderInstance {
+                                flags: base_flags | ((clip_kind as i32) << 24),
+                                clip_params: params,
+                                ..base_instance
+                            });
+                        }
                     }
                 }
                 None => {
                     instances.push(base_instance);
                 }
             }
         }
         BorderSegment::Top |
         BorderSegment::Bottom |
         BorderSegment::Right |
         BorderSegment::Left => {
             let is_vertical = segment == BorderSegment::Left ||
                               segment == BorderSegment::Right;
 
             match style0 {
                 BorderStyle::Dashed => {
-                    let rect = if is_vertical {
-                        let half_dash_size = task_rect.size.height * 0.5;
-                        let y0 = task_rect.origin.y;
-                        let y1 = y0 + half_dash_size.round();
-
-                        DeviceRect::from_floats(
-                            task_rect.origin.x,
-                            y0,
-                            task_rect.origin.x + task_rect.size.width,
-                            y1,
-                        )
+                    let (x, y) = if is_vertical {
+                        let half_dash_size = task_rect.size.height * 0.25;
+                        (0., half_dash_size)
                     } else {
-                        let half_dash_size = task_rect.size.width * 0.5;
-                        let x0 = task_rect.origin.x;
-                        let x1 = x0 + half_dash_size.round();
-
-                        DeviceRect::from_floats(
-                            x0,
-                            task_rect.origin.y,
-                            x1,
-                            task_rect.origin.y + task_rect.size.height,
-                        )
+                        let half_dash_size = task_rect.size.width * 0.25;
+                        (half_dash_size, 0.)
                     };
 
                     instances.push(BorderInstance {
-                        local_rect: rect,
+                        flags: base_flags | ((BorderClipKind::DashEdge as i32) << 24),
+                        clip_params: [
+                            x, y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+                        ],
                         ..base_instance
                     });
                 }
                 BorderStyle::Dotted => {
                     let (x, y, r) = if is_vertical {
                         (widths.width * 0.5,
                          widths.width,
                          widths.width * 0.5)
                     } else {
                         (widths.height,
                          widths.height * 0.5,
                          widths.height * 0.5)
                     };
 
                     instances.push(BorderInstance {
-                        flags: base_flags | ((BorderCornerClipKind::Dot as i32) << 24),
+                        flags: base_flags | ((BorderClipKind::Dot as i32) << 24),
                         clip_params: [
                             x, y, r, 0.0, 0.0, 0.0, 0.0, 0.0,
                         ],
                         ..base_instance
                     });
                 }
                 _ => {
                     instances.push(base_instance);
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -323,16 +323,19 @@ pub struct ClipStore {
 #[derive(Debug)]
 pub struct ClipChainInstance {
     pub clips_range: ClipNodeRange,
     // Combined clip rect for clips that are in the
     // same coordinate system as the primitive.
     pub local_clip_rect: LayoutRect,
     pub has_non_root_coord_system: bool,
     pub has_non_local_clips: bool,
+    // If true, this clip chain requires allocation
+    // of a clip mask.
+    pub needs_mask: bool,
     // Combined clip rect in picture space (may
     // be more conservative that local_clip_rect).
     pub pic_clip_rect: PictureRect,
 }
 
 impl ClipStore {
     pub fn new() -> Self {
         ClipStore {
@@ -519,16 +522,17 @@ impl ClipStore {
         // Now, we've collected all the clip nodes that *potentially* affect this
         // primitive region, and reduced the size of the prim region as much as possible.
 
         // Run through the clip nodes, and see which ones affect this prim region.
 
         let first_clip_node_index = self.clip_node_indices.len() as u32;
         let mut has_non_root_coord_system = false;
         let mut has_non_local_clips = false;
+        let mut needs_mask = false;
 
         // For each potential clip node
         for node_info in self.clip_node_info.drain(..) {
             let node = &mut self.clip_nodes[node_info.node_index.0 as usize];
 
             // See how this clip affects the prim region.
             let clip_result = match node_info.conversion {
                 ClipSpaceConversion::Local => {
@@ -575,16 +579,36 @@ impl ClipStore {
                         ClipSpaceConversion::Offset(..) => {
                             ClipNodeFlags::SAME_COORD_SYSTEM
                         }
                         ClipSpaceConversion::Transform(..) => {
                             ClipNodeFlags::empty()
                         }
                     };
 
+                    // As a special case, a partial accept of a clip rect that is
+                    // in the same coordinate system as the primitive doesn't need
+                    // a clip mask. Instead, it can be handled by the primitive
+                    // vertex shader as part of the local clip rect. This is an
+                    // important optimization for reducing the number of clip
+                    // masks that are allocated on common pages.
+                    needs_mask |= match node.item {
+                        ClipItem::Rectangle(_, ClipMode::ClipOut) |
+                        ClipItem::RoundedRectangle(..) |
+                        ClipItem::Image(..) |
+                        ClipItem::BoxShadow(..) |
+                        ClipItem::LineDecoration(..) => {
+                            true
+                        }
+
+                        ClipItem::Rectangle(_, ClipMode::Clip) => {
+                            !flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM)
+                        }
+                    };
+
                     // Store this in the index buffer for this clip chain instance.
                     self.clip_node_indices
                         .push(ClipNodeInstance::new(node_info.node_index, flags));
 
                     has_non_root_coord_system |= node_info.has_non_root_coord_system;
                 }
             }
         }
@@ -597,16 +621,17 @@ impl ClipStore {
 
         // Return a valid clip chain instance
         Some(ClipChainInstance {
             clips_range,
             has_non_root_coord_system,
             has_non_local_clips,
             local_clip_rect,
             pic_clip_rect,
+            needs_mask,
         })
     }
 }
 
 #[derive(Debug)]
 pub struct LineDecorationClipSource {
     rect: LayoutRect,
     style: LineStyle,
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -102,19 +102,16 @@ pub struct DisplayListFlattener<'a> {
 
     /// The data structure that converting between ClipId and the various index
     /// types that the ClipScrollTree uses.
     id_to_index_mapper: ClipIdToIndexMapper,
 
     /// A stack of stacking context properties.
     sc_stack: Vec<FlattenedStackingContext>,
 
-    /// A stack of the current pictures.
-    picture_stack: Vec<PrimitiveIndex>,
-
     /// A stack of the currently active shadows
     shadow_stack: Vec<(Shadow, PrimitiveIndex)>,
 
     /// The stack keeping track of the root clip chains associated with pipelines.
     pipeline_clip_chain_stack: Vec<ClipChainId>,
 
     /// A list of scrollbar primitives.
     pub scrollbar_prims: Vec<ScrollbarPrimitive>,
@@ -159,34 +156,33 @@ impl<'a> DisplayListFlattener<'a> {
             scene,
             clip_scroll_tree,
             font_instances,
             config: *frame_builder_config,
             output_pipelines,
             id_to_index_mapper: ClipIdToIndexMapper::default(),
             hit_testing_runs: recycle_vec(old_builder.hit_testing_runs),
             scrollbar_prims: recycle_vec(old_builder.scrollbar_prims),
-            picture_stack: Vec::new(),
             shadow_stack: Vec::new(),
             sc_stack: Vec::new(),
             next_picture_id: old_builder.next_picture_id,
             pipeline_clip_chain_stack: vec![ClipChainId::NONE],
             prim_store: old_builder.prim_store.recycle(),
             clip_store: old_builder.clip_store.recycle(),
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
         flattener.setup_viewport_offset(view.inner_rect, view.accumulated_scale_factor());
         flattener.flatten_root(root_pipeline, &root_pipeline.viewport_size);
 
-        debug_assert!(flattener.picture_stack.is_empty());
+        debug_assert!(flattener.sc_stack.is_empty());
 
         new_scene.root_pipeline_id = Some(root_pipeline_id);
         new_scene.pipeline_epochs = scene.pipeline_epochs.clone();
         new_scene.pipelines = scene.pipelines.clone();
 
         FrameBuilder::with_display_list_flattener(
             view.inner_rect,
             background_color,
@@ -813,17 +809,17 @@ impl<'a> DisplayListFlattener<'a> {
     }
 
     /// Add an already created primitive to the draw lists.
     pub fn add_primitive_to_draw_list(
         &mut self,
         prim_index: PrimitiveIndex,
     ) {
         // Add primitive to the top-most Picture on the stack.
-        let pic_prim_index = *self.picture_stack.last().unwrap();
+        let pic_prim_index = self.sc_stack.last().unwrap().leaf_prim_index;
         let pic = self.prim_store.get_pic_mut(pic_prim_index);
         pic.add_primitive(prim_index);
     }
 
     /// Convenience interface that creates a primitive entry and adds it
     /// to the draw list.
     pub fn add_primitive(
         &mut self,
@@ -917,61 +913,16 @@ impl<'a> DisplayListFlattener<'a> {
         // An arbitrary large clip rect. For now, we don't
         // specify a clip specific to the stacking context.
         // However, now that they are represented as Picture
         // primitives, we can apply any kind of clip mask
         // to them, as for a normal primitive. This is needed
         // to correctly handle some CSS cases (see #1957).
         let max_clip = LayoutRect::max_rect();
 
-        // If there is no root picture, create one for the main framebuffer.
-        if self.sc_stack.is_empty() {
-            // Should be no pictures at all if the stack is empty...
-            debug_assert!(self.prim_store.primitives.is_empty());
-            debug_assert_eq!(transform_style, TransformStyle::Flat);
-
-            // This picture stores primitive runs for items on the
-            // main framebuffer.
-            let picture = PicturePrimitive::new_image(
-                self.get_next_picture_id(),
-                None,
-                false,
-                pipeline_id,
-                None,
-                true,
-            );
-
-            let prim_index = self.prim_store.add_primitive(
-                &LayoutRect::zero(),
-                &max_clip,
-                true,
-                ClipChainId::NONE,
-                spatial_node_index,
-                None,
-                PrimitiveContainer::Brush(BrushPrimitive::new_picture(picture)),
-            );
-
-            self.picture_stack.push(prim_index);
-        } else if composite_ops.mix_blend_mode.is_some() && self.sc_stack.len() > 2 {
-            // If we have a mix-blend-mode, and we aren't the primary framebuffer,
-            // the stacking context needs to be isolated to blend correctly as per
-            // the CSS spec.
-            // TODO(gw): The way we detect not being the primary framebuffer (len > 2)
-            //           is hacky and depends on how we create a root stacking context
-            //           during flattening.
-            let parent_prim_index = *self.picture_stack.last().unwrap();
-            let parent_pic = self.prim_store.get_pic_mut(parent_prim_index);
-
-            // If not already isolated for some other reason,
-            // make this picture as isolated.
-            if parent_pic.requested_composite_mode.is_none() {
-                parent_pic.requested_composite_mode = Some(PictureCompositeMode::Blit);
-            }
-        }
-
         // Get the transform-style of the parent stacking context,
         // which determines if we *might* need to draw this on
         // an intermediate surface for plane splitting purposes.
         let parent_transform_style = match self.sc_stack.last() {
             Some(sc) => sc.transform_style,
             None => TransformStyle::Flat,
         };
 
@@ -987,136 +938,23 @@ impl<'a> DisplayListFlattener<'a> {
 
         // If this is participating in a 3d context *and* the
         // parent was not a 3d context, then this must be the
         // element that establishes a new 3d context.
         let establishes_3d_context =
             participating_in_3d_context &&
             parent_transform_style == TransformStyle::Flat;
 
-        let rendering_context_3d_prim_index = if establishes_3d_context {
-            // If establishing a 3d context, we need to add a picture
-            // that will be the container for all the planes and any
-            // un-transformed content.
-            let picture = PicturePrimitive::new_image(
-                self.get_next_picture_id(),
-                None,
-                false,
-                pipeline_id,
-                None,
-                true,
-            );
-
-            let prim = BrushPrimitive::new_picture(picture);
-
-            let prim_index = self.prim_store.add_primitive(
-                &LayoutRect::zero(),
-                &max_clip,
-                true,
-                clip_chain_id,
-                spatial_node_index,
-                None,
-                PrimitiveContainer::Brush(prim),
-            );
-
-            let parent_prim_index = *self.picture_stack.last().unwrap();
-
-            let pic = self.prim_store.get_pic_mut(parent_prim_index);
-            pic.add_primitive(prim_index);
-
-            self.picture_stack.push(prim_index);
-
-            Some(prim_index)
-        } else {
-            None
-        };
-
-        let mut parent_prim_index = if !establishes_3d_context && participating_in_3d_context {
-            // If we're in a 3D context, we will parent the picture
-            // to the first stacking context we find that is a
-            // 3D rendering context container. This follows the spec
-            // by hoisting these items out into the same 3D context
-            // for plane splitting.
-            self.sc_stack
-                .iter()
-                .rev()
-                .find(|sc| sc.rendering_context_3d_prim_index.is_some())
-                .map(|sc| sc.rendering_context_3d_prim_index.unwrap())
-                .unwrap()
-        } else {
-            *self.picture_stack.last().unwrap()
-        };
-
-        // Same for mix-blend-mode.
-        if let Some(mix_blend_mode) = composite_ops.mix_blend_mode {
-            let picture = PicturePrimitive::new_image(
-                self.get_next_picture_id(),
-                Some(PictureCompositeMode::MixBlend(mix_blend_mode)),
-                false,
-                pipeline_id,
-                None,
-                true,
-            );
-
-            let src_prim = BrushPrimitive::new_picture(picture);
-
-            let src_prim_index = self.prim_store.add_primitive(
-                &LayoutRect::zero(),
-                &max_clip,
-                true,
-                clip_chain_id,
-                spatial_node_index,
-                None,
-                PrimitiveContainer::Brush(src_prim),
-            );
-
-            let parent_pic = self.prim_store.get_pic_mut(parent_prim_index);
-            parent_prim_index = src_prim_index;
-            parent_pic.add_primitive(src_prim_index);
-
-            self.picture_stack.push(src_prim_index);
-        }
-
-        // For each filter, create a new image with that composite mode.
-        for filter in composite_ops.filters.iter().rev() {
-            let picture = PicturePrimitive::new_image(
-                self.get_next_picture_id(),
-                Some(PictureCompositeMode::Filter(*filter)),
-                false,
-                pipeline_id,
-                None,
-                true,
-            );
-
-            let src_prim = BrushPrimitive::new_picture(picture);
-            let src_prim_index = self.prim_store.add_primitive(
-                &LayoutRect::zero(),
-                &max_clip,
-                true,
-                clip_chain_id,
-                spatial_node_index,
-                None,
-                PrimitiveContainer::Brush(src_prim),
-            );
-
-            let parent_pic = self.prim_store.get_pic_mut(parent_prim_index);
-            parent_prim_index = src_prim_index;
-
-            parent_pic.add_primitive(src_prim_index);
-
-            self.picture_stack.push(src_prim_index);
-        }
-
         // By default, this picture will be collapsed into
         // the owning target.
         let mut composite_mode = None;
+
+        // If this stacking context is the root of a pipeline, and the caller
+        // has requested it as an output frame, create a render task to isolate it.
         let mut frame_output_pipeline_id = None;
-
-        // If this stacking context if the root of a pipeline, and the caller
-        // has requested it as an output frame, create a render task to isolate it.
         if is_pipeline_root && self.output_pipelines.contains(&pipeline_id) {
             composite_mode = Some(PictureCompositeMode::Blit);
             frame_output_pipeline_id = Some(pipeline_id);
         }
 
         // Force an intermediate surface if the stacking context
         // has a clip node. In the future, we may decide during
         // prepare step to skip the intermediate surface if the
@@ -1128,86 +966,181 @@ impl<'a> DisplayListFlattener<'a> {
             //           there is a large optimization opportunity here.
             //           During culling, we can check if there is actually
             //           perspective present, and skip the plane splitting
             //           completely when that is not the case.
             composite_mode = Some(PictureCompositeMode::Blit);
         }
 
         // Add picture for this actual stacking context contents to render into.
-        let picture = PicturePrimitive::new_image(
+        let leaf_picture = PicturePrimitive::new_image(
             self.get_next_picture_id(),
             composite_mode,
             participating_in_3d_context,
             pipeline_id,
             frame_output_pipeline_id,
             true,
         );
 
         // Create a brush primitive that draws this picture.
-        let sc_prim = BrushPrimitive::new_picture(picture);
+        let leaf_prim = BrushPrimitive::new_picture(leaf_picture);
 
         // Add the brush to the parent picture.
-        let sc_prim_index = self.prim_store.add_primitive(
+        let leaf_prim_index = self.prim_store.add_primitive(
             &LayoutRect::zero(),
             &max_clip,
             true,
             clip_chain_id,
             spatial_node_index,
             None,
-            PrimitiveContainer::Brush(sc_prim),
+            PrimitiveContainer::Brush(leaf_prim),
         );
 
-        let parent_pic = self.prim_store.get_pic_mut(parent_prim_index);
-        parent_pic.add_primitive(sc_prim_index);
+        // Create a chain of pictures based on presence of filters,
+        // mix-blend-mode and/or 3d rendering context containers.
+        let mut current_prim_index = leaf_prim_index;
+
+        // For each filter, create a new image with that composite mode.
+        for filter in &composite_ops.filters {
+            let mut filter_picture = PicturePrimitive::new_image(
+                self.get_next_picture_id(),
+                Some(PictureCompositeMode::Filter(*filter)),
+                false,
+                pipeline_id,
+                None,
+                true,
+            );
+
+            filter_picture.add_primitive(current_prim_index);
+            let filter_prim = BrushPrimitive::new_picture(filter_picture);
+
+            current_prim_index = self.prim_store.add_primitive(
+                &LayoutRect::zero(),
+                &max_clip,
+                true,
+                clip_chain_id,
+                spatial_node_index,
+                None,
+                PrimitiveContainer::Brush(filter_prim),
+            );
+        }
+
+        // Same for mix-blend-mode.
+        if let Some(mix_blend_mode) = composite_ops.mix_blend_mode {
+            let mut blend_picture = PicturePrimitive::new_image(
+                self.get_next_picture_id(),
+                Some(PictureCompositeMode::MixBlend(mix_blend_mode)),
+                false,
+                pipeline_id,
+                None,
+                true,
+            );
 
-        // Add this as the top-most picture for primitives to be added to.
-        self.picture_stack.push(sc_prim_index);
+            blend_picture.add_primitive(current_prim_index);
+            let blend_prim = BrushPrimitive::new_picture(blend_picture);
+
+            current_prim_index = self.prim_store.add_primitive(
+                &LayoutRect::zero(),
+                &max_clip,
+                true,
+                clip_chain_id,
+                spatial_node_index,
+                None,
+                PrimitiveContainer::Brush(blend_prim),
+            );
+        }
+
+        if establishes_3d_context {
+            // If establishing a 3d context, we need to add a picture
+            // that will be the container for all the planes and any
+            // un-transformed content.
+            let mut container_picture = PicturePrimitive::new_image(
+                self.get_next_picture_id(),
+                None,
+                false,
+                pipeline_id,
+                None,
+                true,
+            );
+
+            container_picture.add_primitive(current_prim_index);
+            let container_prim = BrushPrimitive::new_picture(container_picture);
+
+            current_prim_index = self.prim_store.add_primitive(
+                &LayoutRect::zero(),
+                &max_clip,
+                true,
+                clip_chain_id,
+                spatial_node_index,
+                None,
+                PrimitiveContainer::Brush(container_prim),
+            );
+        }
 
         // Push the SC onto the stack, so we know how to handle things in
         // pop_stacking_context.
         let sc = FlattenedStackingContext {
-            composite_ops,
             is_backface_visible,
             pipeline_id,
             transform_style,
-            rendering_context_3d_prim_index,
+            establishes_3d_context,
+            participating_in_3d_context,
+            leaf_prim_index,
+            root_prim_index: current_prim_index,
             glyph_raster_space,
+            has_mix_blend_mode: composite_ops.mix_blend_mode.is_some(),
         };
 
         self.sc_stack.push(sc);
     }
 
     pub fn pop_stacking_context(&mut self) {
         let sc = self.sc_stack.pop().unwrap();
 
-        // Always pop at least the main picture for this stacking context.
-        let mut pop_count = 1;
-
-        // Remove the picture for any filter/mix-blend-mode effects.
-        pop_count += sc.composite_ops.count();
-
-        // Remove the 3d context container if created
-        if sc.rendering_context_3d_prim_index.is_some() {
-            pop_count += 1;
-        }
-
-        for _ in 0 .. pop_count {
-            let prim_index = self
-                .picture_stack
-                .pop()
-                .expect("bug: mismatched picture stack");
+        // Run the optimize pass on each picture in the chain,
+        // to see if we can collapse opacity and avoid drawing
+        // to an off-screen surface.
+        for i in sc.leaf_prim_index.0 .. sc.root_prim_index.0 + 1 {
+            let prim_index = PrimitiveIndex(i);
             self.prim_store.optimize_picture_if_possible(prim_index);
         }
 
-        // By the time the stacking context stack is empty, we should
-        // also have cleared the picture stack.
         if self.sc_stack.is_empty() {
-            self.picture_stack.pop().expect("bug: picture stack invalid");
-            debug_assert!(self.picture_stack.is_empty());
+            // This must be the root stacking context
+            return;
+        }
+
+        let parent_prim_index = if !sc.establishes_3d_context && sc.participating_in_3d_context {
+            // If we're in a 3D context, we will parent the picture
+            // to the first stacking context we find that is a
+            // 3D rendering context container. This follows the spec
+            // by hoisting these items out into the same 3D context
+            // for plane splitting.
+            self.sc_stack
+                .iter()
+                .rev()
+                .find(|sc| sc.establishes_3d_context)
+                .map(|sc| sc.root_prim_index)
+                .unwrap()
+        } else {
+            self.sc_stack.last().unwrap().leaf_prim_index
+        };
+
+        let parent_pic = self.prim_store.get_pic_mut(parent_prim_index);
+        parent_pic.add_primitive(sc.root_prim_index);
+
+        // If we have a mix-blend-mode, and we aren't the primary framebuffer,
+        // the stacking context needs to be isolated to blend correctly as per
+        // the CSS spec.
+        // If not already isolated for some other reason,
+        // make this picture as isolated.
+        if sc.has_mix_blend_mode &&
+           self.sc_stack.len() > 2 &&
+           parent_pic.requested_composite_mode.is_none() {
+            parent_pic.requested_composite_mode = Some(PictureCompositeMode::Blit);
         }
 
         assert!(
             self.shadow_stack.is_empty(),
             "Found unpopped text shadows when popping stacking context!"
         );
     }
 
@@ -2001,29 +1934,30 @@ impl<'a> DisplayListFlattener<'a> {
 
 /// Properties of a stacking context that are maintained
 /// during creation of the scene. These structures are
 /// not persisted after the initial scene build.
 struct FlattenedStackingContext {
     /// Pipeline this stacking context belongs to.
     pipeline_id: PipelineId,
 
-    /// Filters / mix-blend-mode effects
-    composite_ops: CompositeOps,
-
     /// If true, visible when backface is visible.
     is_backface_visible: bool,
 
     /// The rasterization mode for any text runs that are part
     /// of this stacking context.
     glyph_raster_space: GlyphRasterSpace,
 
     /// CSS transform-style property.
     transform_style: TransformStyle,
 
-    /// If Some(..), this stacking context establishes a new
-    /// 3d rendering context, and the value is the picture
-    // index of the 3d context container.
-    rendering_context_3d_prim_index: Option<PrimitiveIndex>,
+    root_prim_index: PrimitiveIndex,
+    leaf_prim_index: PrimitiveIndex,
+
+    /// If true, this stacking context establishes a new
+    /// 3d rendering context.
+    establishes_3d_context: bool,
+    participating_in_3d_context: bool,
+    has_mix_blend_mode: bool,
 }
 
 #[derive(Debug)]
 pub struct ScrollbarInfo(pub SpatialNodeIndex, pub LayoutRect);
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -2240,17 +2240,17 @@ impl Primitive {
                     frame_state.resource_cache,
                     frame_context.device_pixel_scale,
                     &frame_context.world_rect,
                     clip_node_collector,
                 );
 
             match segment_clip_chain {
                 Some(segment_clip_chain) => {
-                    if segment_clip_chain.clips_range.count == 0 ||
+                    if !segment_clip_chain.needs_mask ||
                        (!segment.may_need_clip_mask && !segment_clip_chain.has_non_local_clips) {
                         segment.clip_task_id = BrushSegmentTaskId::Opaque;
                         continue;
                     }
 
                     let (device_rect, _, _) = match get_raster_rects(
                         segment_clip_chain.pic_clip_rect,
                         &pic_state.map_pic_to_raster,
@@ -2789,17 +2789,17 @@ impl Primitive {
             clip_node_collector,
         ) {
             if cfg!(debug_assertions) && is_chased {
                 println!("\tsegment tasks have been created for clipping");
             }
             return;
         }
 
-        if clip_chain.clips_range.count > 0 {
+        if clip_chain.needs_mask {
             if let Some((device_rect, _, _)) = get_raster_rects(
                 clip_chain.pic_clip_rect,
                 &pic_state.map_pic_to_raster,
                 &pic_state.map_raster_to_world,
                 prim_bounding_rect,
                 frame_context.device_pixel_scale,
             ) {
                 let clip_task = RenderTask::new_mask(
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -748,17 +748,41 @@ impl RenderBackend {
 
                         for (id, doc) in &self.documents {
                             let captured = CapturedDocument {
                                 document_id: *id,
                                 root_pipeline_id: doc.scene.root_pipeline_id,
                                 window_size: doc.view.window_size,
                             };
                             tx.send(captured).unwrap();
+
+                            // notify the active recorder
+                            if let Some(ref mut r) = self.recorder {
+                                let pipeline_id = doc.scene.root_pipeline_id.unwrap();
+                                let epoch =  doc.scene.pipeline_epochs[&pipeline_id];
+                                let pipeline = &doc.scene.pipelines[&pipeline_id];
+                                let scene_msg = SceneMsg::SetDisplayList {
+                                    list_descriptor: pipeline.display_list.descriptor().clone(),
+                                    epoch,
+                                    pipeline_id,
+                                    background: pipeline.background_color,
+                                    viewport_size: pipeline.viewport_size,
+                                    content_size: pipeline.content_size,
+                                    preserve_frame_state: false,
+                                };
+                                let txn = TransactionMsg::scene_message(scene_msg);
+                                r.write_msg(*frame_counter, &ApiMsg::UpdateDocument(*id, txn));
+                                r.write_payload(*frame_counter, &Payload::construct_data(
+                                    epoch,
+                                    pipeline_id,
+                                    pipeline.display_list.data(),
+                                ));
+                            }
                         }
+
                         // Note: we can't pass `LoadCapture` here since it needs to arrive
                         // before the `PublishDocument` messages sent by `load_capture`.
                         return true;
                     }
                     DebugCommand::ClearCaches(mask) => {
                         self.resource_cache.clear(mask);
                         return true;
                     }
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -2043,29 +2043,25 @@ impl Renderer {
         );
         debug_target.add(
             debug_server::BatchKind::Cache,
             "Horizontal Blur",
             target.horizontal_blurs.len(),
         );
 
         for alpha_batch_container in &target.alpha_batch_containers {
-            for batch in alpha_batch_container
-                .opaque_batches
-                .iter()
-                .rev() {
+            for batch in alpha_batch_container.opaque_batches.iter().rev() {
                 debug_target.add(
                     debug_server::BatchKind::Opaque,
                     batch.key.kind.debug_name(),
                     batch.instances.len(),
                 );
             }
 
-            for batch in &alpha_batch_container
-                .alpha_batches {
+            for batch in &alpha_batch_container.alpha_batches {
                 debug_target.add(
                     debug_server::BatchKind::Alpha,
                     batch.key.kind.debug_name(),
                     batch.instances.len(),
                 );
             }
         }
 
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -377,28 +377,28 @@ impl TransactionMsg {
     pub fn is_empty(&self) -> bool {
         !self.generate_frame &&
             self.scene_ops.is_empty() &&
             self.frame_ops.is_empty() &&
             self.resource_updates.is_empty()
     }
 
     // TODO: We only need this for a few RenderApi methods which we should remove.
-    fn frame_message(msg: FrameMsg) -> Self {
+    pub fn frame_message(msg: FrameMsg) -> Self {
         TransactionMsg {
             scene_ops: Vec::new(),
             frame_ops: vec![msg],
             resource_updates: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
             low_priority: false,
         }
     }
 
-    fn scene_message(msg: SceneMsg) -> Self {
+    pub fn scene_message(msg: SceneMsg) -> Self {
         TransactionMsg {
             scene_ops: vec![msg],
             frame_ops: Vec::new(),
             resource_updates: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
             low_priority: false,
         }
--- a/gfx/webrender_api/src/channel.rs
+++ b/gfx/webrender_api/src/channel.rs
@@ -19,31 +19,36 @@ pub struct Payload {
     /// A pipeline id to key the payload with, along with the epoch.
     pub pipeline_id: PipelineId,
     pub display_list_data: Vec<u8>,
 }
 
 impl Payload {
     /// Convert the payload to a raw byte vector, in order for it to be
     /// efficiently shared via shmem, for example.
+    /// This is a helper static method working on a slice.
+    pub fn construct_data(epoch: Epoch, pipeline_id: PipelineId, dl_data: &[u8]) -> Vec<u8> {
+        let mut data = Vec::with_capacity(
+            mem::size_of::<u32>() + 2 * mem::size_of::<u32>() + mem::size_of::<u64>() + dl_data.len(),
+        );
+        data.write_u32::<LittleEndian>(epoch.0).unwrap();
+        data.write_u32::<LittleEndian>(pipeline_id.0).unwrap();
+        data.write_u32::<LittleEndian>(pipeline_id.1).unwrap();
+        data.write_u64::<LittleEndian>(dl_data.len() as u64)
+            .unwrap();
+        data.extend_from_slice(dl_data);
+        data
+    }
+    /// Convert the payload to a raw byte vector, in order for it to be
+    /// efficiently shared via shmem, for example.
     ///
     /// TODO(emilio, #1049): Consider moving the IPC boundary to the
     /// constellation in Servo and remove this complexity from WR.
     pub fn to_data(&self) -> Vec<u8> {
-        let mut data = Vec::with_capacity(
-            mem::size_of::<u32>() + 2 * mem::size_of::<u32>() + mem::size_of::<u64>() +
-                self.display_list_data.len(),
-        );
-        data.write_u32::<LittleEndian>(self.epoch.0).unwrap();
-        data.write_u32::<LittleEndian>(self.pipeline_id.0).unwrap();
-        data.write_u32::<LittleEndian>(self.pipeline_id.1).unwrap();
-        data.write_u64::<LittleEndian>(self.display_list_data.len() as u64)
-            .unwrap();
-        data.extend_from_slice(&self.display_list_data);
-        data
+        Self::construct_data(self.epoch, self.pipeline_id, &self.display_list_data)
     }
 
     /// Deserializes the given payload from a raw byte vector.
     pub fn from_data(data: &[u8]) -> Payload {
         let mut payload_reader = Cursor::new(data);
         let epoch = Epoch(payload_reader.read_u32::<LittleEndian>().unwrap());
         let pipeline_id = PipelineId(
             payload_reader.read_u32::<LittleEndian>().unwrap(),
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-04d63e7d73b9661d9eb934a0933c8f9751a9a3db
+02f14d0f333ef125d1abff7b1146039a0ba75f43