Bug 1519718 - WR mix-blend rewrite r=gw
authorDzmitry Malyshau <dmalyshau@mozilla.com>
Mon, 25 Feb 2019 00:17:22 +0000
changeset 460835 9bae35dc1471ff97f0efba49083bb165453ebf35
parent 460834 b73f033efb41e21920b61fb97690fc3499d3f64b
child 460836 d5643033fdd801efda790a2836a3e196e7ba442e
push id35608
push useropoprus@mozilla.com
push dateMon, 25 Feb 2019 10:22:07 +0000
treeherdermozilla-central@9f35adb8e466 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1519718
milestone67.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 1519718 - WR mix-blend rewrite r=gw This is a new implementation of mix-blend compositing that is meant to be more idiomatic to WR and efficient. Previously, mix-blend mode was composed in the following way: 1. parent stacking context was forced to isolate 2. source picture is also isolated 3. when rendering the isolated context, the framebuffer is read upon reaching the source. Both the readback and the source are placed in the RT cache. 4. a mix-blend draw call is issued to read from those cache segments and blend on top of the backdrop The new implementation works by using the picture cutting (intruduced for preserve-3D contexts earlier) and some bits of magic: 1. backdrop stacking context is isolated with a special composition mode that prevents it from actually rendeing unless the suorce stacking context is invisible. 2. source stacking context is isolated with mix-blend composition mode that has a pointer to the backdrop picture 3. the instance of the backdrop picture is placed as a peer of the source picture (not a child) 4. if the backdrop is invisible, the source is drawn as a simple blit 5. otherwise, it's a draw call that reads from the isolated backdrop and source textures Note the differences: - parent stacking context is not isolated, but backdrop is - no framebuffer readback is involved - the source and backdrop pictures are rendered in parallel in a pass, improving the batching - we don't blend onto the backdrop while reading from the backdrop copy at the same time - the depth of the render pass tree is reduced: previously the parent and the source were isolated, now the source and the backdrop, which are siblings Differential Revision: https://phabricator.services.mozilla.com/D20608
gfx/wr/webrender/res/brush_mix_blend.glsl
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/clip_scroll_tree.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/picture.rs
gfx/wr/webrender/src/render_task.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/tiling.rs
gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml
gfx/wr/wrench/reftests/blend/multiply-2.yaml
gfx/wr/wrench/reftests/blend/multiply-3-ref.yaml
gfx/wr/wrench/reftests/blend/multiply-3.yaml
gfx/wr/wrench/reftests/blend/multiply-4.yaml
gfx/wr/wrench/reftests/blend/reftest.list
gfx/wr/wrench/reftests/blend/transform-source-ref.yaml
gfx/wr/wrench/reftests/blend/transform-source.yaml
gfx/wr/wrench/src/main.rs
layout/reftests/css-blending/reftest.list
--- a/gfx/wr/webrender/res/brush_mix_blend.glsl
+++ b/gfx/wr/webrender/res/brush_mix_blend.glsl
@@ -1,53 +1,63 @@
 /* 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/. */
 
 #define VECS_PER_SPECIFIC_BRUSH 3
 
 #include shared,prim_shared,brush
 
-varying vec3 vSrcUv;
-varying vec3 vBackdropUv;
-flat varying int vOp;
+varying vec4 vSourceAndBackdropUv;
+flat varying ivec4 vSourceUvBounds;
+flat varying ivec4 vBackdropUvBounds;
+flat varying ivec3 vOpAndLayers;
 
 #ifdef WR_VERTEX_SHADER
 
 //Note: this function is unsafe for `vi.world_pos.w <= 0.0`
 vec2 snap_device_pos(VertexInfo vi, float device_pixel_scale) {
     return vi.world_pos.xy * device_pixel_scale / max(0.0, vi.world_pos.w) + vi.snap_offset;
 }
 
+ivec4 rect_to_ivec(RectWithSize rect) {
+    return ivec4(rect.p0, rect.p0 + rect.size - 1.0);
+}
+
 void brush_vs(
     VertexInfo vi,
     int prim_address,
     RectWithSize local_rect,
     RectWithSize segment_rect,
     ivec4 user_data,
     mat4 transform,
     PictureTask pic_task,
     int brush_flags,
     vec4 unused
 ) {
     vec2 snapped_device_pos = snap_device_pos(vi, pic_task.device_pixel_scale);
-    vec2 texture_size = vec2(textureSize(sPrevPassColor, 0));
-    vOp = user_data.x;
 
     PictureTask src_task = fetch_picture_task(user_data.z);
+    PictureTask backdrop_task = fetch_picture_task(user_data.y);
+
     vec2 src_uv = snapped_device_pos +
                   src_task.common_data.task_rect.p0 -
                   src_task.content_origin;
-    vSrcUv = vec3(src_uv / texture_size, src_task.common_data.texture_layer_index);
+    vec2 backdrop_uv = snapped_device_pos +
+                       backdrop_task.common_data.task_rect.p0 -
+                       backdrop_task.content_origin;
 
-    RenderTaskCommonData backdrop_task = fetch_render_task_common_data(user_data.y);
-    vec2 backdrop_uv = snapped_device_pos +
-                       backdrop_task.task_rect.p0 -
-                       src_task.content_origin;
-    vBackdropUv = vec3(backdrop_uv / texture_size, backdrop_task.texture_layer_index);
+    vSourceAndBackdropUv = vec4(src_uv, backdrop_uv);
+    vSourceUvBounds = rect_to_ivec(src_task.common_data.task_rect);
+    vBackdropUvBounds = rect_to_ivec(backdrop_task.common_data.task_rect);
+    vOpAndLayers = ivec3(
+        user_data.x,
+        int(src_task.common_data.texture_layer_index),
+        int(backdrop_task.common_data.texture_layer_index)
+    );
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 vec3 Multiply(vec3 Cb, vec3 Cs) {
     return Cb * Cs;
 }
 
@@ -200,87 +210,94 @@ const int MixBlendMode_SoftLight   = 9;
 const int MixBlendMode_Difference  = 10;
 const int MixBlendMode_Exclusion   = 11;
 const int MixBlendMode_Hue         = 12;
 const int MixBlendMode_Saturation  = 13;
 const int MixBlendMode_Color       = 14;
 const int MixBlendMode_Luminosity  = 15;
 
 Fragment brush_fs() {
-    vec4 Cb = textureLod(sPrevPassColor, vBackdropUv, 0.0);
-    vec4 Cs = textureLod(sPrevPassColor, vSrcUv, 0.0);
-
-    // The mix-blend-mode functions assume no premultiplied alpha
-    if (Cb.a != 0.0) {
-        Cb.rgb /= Cb.a;
-    }
-
-    if (Cs.a != 0.0) {
-        Cs.rgb /= Cs.a;
-    }
-
     // Return yellow if none of the branches match (shouldn't happen).
     vec4 result = vec4(1.0, 1.0, 0.0, 1.0);
 
-    switch (vOp) {
-        case MixBlendMode_Multiply:
-            result.rgb = Multiply(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Screen:
-            result.rgb = Screen(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Overlay:
-            // Overlay is inverse of Hardlight
-            result.rgb = HardLight(Cs.rgb, Cb.rgb);
-            break;
-        case MixBlendMode_Darken:
-            result.rgb = min(Cs.rgb, Cb.rgb);
-            break;
-        case MixBlendMode_Lighten:
-            result.rgb = max(Cs.rgb, Cb.rgb);
-            break;
-        case MixBlendMode_ColorDodge:
-            result.r = ColorDodge(Cb.r, Cs.r);
-            result.g = ColorDodge(Cb.g, Cs.g);
-            result.b = ColorDodge(Cb.b, Cs.b);
-            break;
-        case MixBlendMode_ColorBurn:
-            result.r = ColorBurn(Cb.r, Cs.r);
-            result.g = ColorBurn(Cb.g, Cs.g);
-            result.b = ColorBurn(Cb.b, Cs.b);
-            break;
-        case MixBlendMode_HardLight:
-            result.rgb = HardLight(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_SoftLight:
-            result.r = SoftLight(Cb.r, Cs.r);
-            result.g = SoftLight(Cb.g, Cs.g);
-            result.b = SoftLight(Cb.b, Cs.b);
-            break;
-        case MixBlendMode_Difference:
-            result.rgb = Difference(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Exclusion:
-            result.rgb = Exclusion(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Hue:
-            result.rgb = Hue(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Saturation:
-            result.rgb = Saturation(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Color:
-            result.rgb = Color(Cb.rgb, Cs.rgb);
-            break;
-        case MixBlendMode_Luminosity:
-            result.rgb = Luminosity(Cb.rgb, Cs.rgb);
-            break;
-        default: break;
+    ivec2 source_uv = ivec2(floor(vSourceAndBackdropUv.xy));
+    vec4 Cs = source_uv == clamp(source_uv, vSourceUvBounds.xy, vSourceUvBounds.zw) ?
+        texelFetch(sPrevPassColor, ivec3(source_uv, vOpAndLayers.y), 0) :
+        vec4(0.0);
+    ivec2 backdrop_uv = ivec2(floor(vSourceAndBackdropUv.zw));
+    vec4 Cb = backdrop_uv == clamp(backdrop_uv, vBackdropUvBounds.xy, vBackdropUvBounds.zw) ?
+        texelFetch(sPrevPassColor, ivec3(backdrop_uv, vOpAndLayers.z), 0) :
+        vec4(0.0);
+
+    if (Cs.a == 0.0) {
+        result = Cb;
+    } else if (Cb.a == 0.0) {
+        result = Cs;
+    } else {
+        vec3 original_backdrop = Cb.rgb;
+        // The mix-blend-mode functions assume no premultiplied alpha
+        Cs.rgb /= Cs.a;
+        Cb.rgb /= Cb.a;
+
+        switch (vOpAndLayers.x) {
+            case MixBlendMode_Multiply:
+                result.rgb = Multiply(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Screen:
+                result.rgb = Screen(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Overlay:
+                // Overlay is inverse of Hardlight
+                result.rgb = HardLight(Cs.rgb, Cb.rgb);
+                break;
+            case MixBlendMode_Darken:
+                result.rgb = min(Cs.rgb, Cb.rgb);
+                break;
+            case MixBlendMode_Lighten:
+                result.rgb = max(Cs.rgb, Cb.rgb);
+                break;
+            case MixBlendMode_ColorDodge:
+                result.r = ColorDodge(Cb.r, Cs.r);
+                result.g = ColorDodge(Cb.g, Cs.g);
+                result.b = ColorDodge(Cb.b, Cs.b);
+                break;
+            case MixBlendMode_ColorBurn:
+                result.r = ColorBurn(Cb.r, Cs.r);
+                result.g = ColorBurn(Cb.g, Cs.g);
+                result.b = ColorBurn(Cb.b, Cs.b);
+                break;
+            case MixBlendMode_HardLight:
+                result.rgb = HardLight(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_SoftLight:
+                result.r = SoftLight(Cb.r, Cs.r);
+                result.g = SoftLight(Cb.g, Cs.g);
+                result.b = SoftLight(Cb.b, Cs.b);
+                break;
+            case MixBlendMode_Difference:
+                result.rgb = Difference(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Exclusion:
+                result.rgb = Exclusion(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Hue:
+                result.rgb = Hue(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Saturation:
+                result.rgb = Saturation(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Color:
+                result.rgb = Color(Cb.rgb, Cs.rgb);
+                break;
+            case MixBlendMode_Luminosity:
+                result.rgb = Luminosity(Cb.rgb, Cs.rgb);
+                break;
+            default: break;
+        }
+
+        vec3 rgb = (1.0 - Cb.a) * Cs.rgb + Cb.a * result.rgb;
+        // simulate alpha-blending with the backdrop
+        result = mix(vec4(original_backdrop, Cb.a), vec4(rgb, 1.0), Cs.a);
     }
 
-    result.rgb = (1.0 - Cb.a) * Cs.rgb + Cb.a * result.rgb;
-    result.a = Cs.a;
-
-    result.rgb *= result.a;
-
     return Fragment(result);
 }
 #endif
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -24,17 +24,17 @@ use render_backend::DataStores;
 use render_task::{RenderTaskAddress, RenderTaskId, RenderTaskTree, TileBlit};
 use renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use renderer::{BLOCKS_PER_UV_RECT, MAX_VERTEX_TEXTURE_WIDTH};
 use resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache, ImageProperties};
 use scene::FilterOpHelpers;
 use smallvec::SmallVec;
 use std::{f32, i32, usize};
 use tiling::{RenderTargetContext};
-use util::{project_rect, TransformedRectKind};
+use util::{project_rect, MaxRect, 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);
 
 /// Used to signal there are no segments provided with this primitive.
 const INVALID_SEGMENT_INDEX: i32 = 0xffff;
 
@@ -974,20 +974,20 @@ impl AlphaBatchBuilder {
                     clip_task_address,
                     transform_id,
                 };
 
                 match picture.context_3d {
                     // Convert all children of the 3D hierarchy root into batches.
                     Picture3DContext::In { root_data: Some(ref list), .. } => {
                         for child in list {
-                            let prim_instance = &picture.prim_list.prim_instances[child.anchor];
-                            let prim_info = &ctx.scratch.prim_info[prim_instance.visibility_info.0 as usize];
+                            let child_prim_instance = &picture.prim_list.prim_instances[child.anchor];
+                            let child_prim_info = &ctx.scratch.prim_info[child_prim_instance.visibility_info.0 as usize];
 
-                            let child_pic_index = match prim_instance.kind {
+                            let child_pic_index = match child_prim_instance.kind {
                                 PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
                                 PrimitiveInstanceKind::LineDecoration { .. } |
                                 PrimitiveInstanceKind::TextRun { .. } |
                                 PrimitiveInstanceKind::NormalBorder { .. } |
                                 PrimitiveInstanceKind::ImageBorder { .. } |
                                 PrimitiveInstanceKind::Rectangle { .. } |
                                 PrimitiveInstanceKind::YuvImage { .. } |
                                 PrimitiveInstanceKind::Image { .. } |
@@ -998,24 +998,24 @@ impl AlphaBatchBuilder {
                                 }
                             };
                             let pic = &ctx.prim_store.pictures[child_pic_index.0];
 
 
                             // Get clip task, if set, for the picture primitive.
                             let clip_task_address = get_clip_task_address(
                                 &ctx.scratch.clip_mask_instances,
-                                prim_info.clip_task_index,
+                                child_prim_info.clip_task_index,
                                 0,
                                 render_tasks,
                             ).unwrap_or(OPAQUE_TASK_ADDRESS);
 
-                            let prim_header = PrimitiveHeader {
+                            let child_header = PrimitiveHeader {
                                 local_rect: pic.local_rect,
-                                local_clip_rect: prim_info.combined_local_clip_rect,
+                                local_clip_rect: child_prim_info.combined_local_clip_rect,
                                 task_address,
                                 specific_prim_address: GpuCacheAddress::invalid(),
                                 clip_task_address,
                                 transform_id: transforms
                                     .get_id(
                                         child.spatial_node_index,
                                         root_spatial_node_index,
                                         ctx.clip_scroll_tree,
@@ -1032,17 +1032,17 @@ impl AlphaBatchBuilder {
                                 .as_ref()
                                 .expect("BUG: no surface")
                                 .resolve(
                                     render_tasks,
                                     ctx.resource_cache,
                                     gpu_cache,
                                 );
 
-                            let prim_header_index = prim_headers.push(&prim_header, z_id, [
+                            let prim_header_index = prim_headers.push(&child_header, z_id, [
                                 uv_rect_address.as_int(),
                                 if raster_config.establishes_raster_root { 1 } else { 0 },
                                 0,
                             ]);
 
                             let key = BatchKey::new(
                                 BatchKind::SplitComposite,
                                 BlendMode::PremultipliedAlpha,
@@ -1052,17 +1052,17 @@ impl AlphaBatchBuilder {
                             let instance = SplitCompositeInstance::new(
                                 prim_header_index,
                                 child.gpu_address,
                                 z_id,
                             );
 
                             self.current_batch_list().push_single_instance(
                                 key,
-                                &prim_info.clip_chain.pic_clip_rect,
+                                &child_prim_info.clip_chain.pic_clip_rect,
                                 z_id,
                                 PrimitiveInstanceData::from(instance),
                             );
                         }
                     }
                     // Ignore the 3D pictures that are not in the root of preserve-3D
                     // hierarchy, since we process them with the root.
                     Picture3DContext::In { root_data: None, .. } => return,
@@ -1122,26 +1122,26 @@ impl AlphaBatchBuilder {
                                     );
 
                                     for tile_index in &tile_cache.tiles_to_draw {
                                         let tile = &tile_cache.tiles[tile_index.0];
 
                                         // Get the local rect of the tile.
                                         let tile_rect = tile.local_rect;
 
-                                        let prim_header = PrimitiveHeader {
+                                        let tile_header = PrimitiveHeader {
                                             local_rect: tile_rect,
                                             local_clip_rect,
                                             task_address,
                                             specific_prim_address: prim_cache_address,
                                             clip_task_address,
                                             transform_id,
                                         };
 
-                                        let prim_header_index = prim_headers.push(&prim_header, z_id, [
+                                        let prim_header_index = prim_headers.push(&tile_header, z_id, [
                                             ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
                                             RasterizationSpace::Local as i32,
                                             get_shader_opacity(1.0),
                                         ]);
 
                                         let cache_item = ctx
                                             .resource_cache
                                             .get_texture_cache_item(&tile.handle);
@@ -1447,59 +1447,108 @@ impl AlphaBatchBuilder {
                                             key,
                                             bounding_rect,
                                             z_id,
                                             PrimitiveInstanceData::from(instance),
                                         );
                                     }
                                 }
                             }
-                            PictureCompositeMode::MixBlend(mode) => {
-                                let surface = ctx.surfaces[raster_config.surface_index.0]
+                            PictureCompositeMode::Puppet { master: Some(source) } if ctx.is_picture_surface_visible(source) => return,
+                            PictureCompositeMode::MixBlend { mode, backdrop } if ctx.is_picture_surface_visible(backdrop) => {
+                                let backdrop_picture = &ctx.prim_store.pictures[backdrop.0];
+
+                                let source_id = ctx
+                                    .surfaces[raster_config.surface_index.0]
                                     .surface
                                     .as_ref()
-                                    .expect("bug: surface must be allocated by now");
-                                let cache_task_id = surface.resolve_render_task_id();
-                                let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
+                                    .expect("bug: source surface must be allocated by now")
+                                    .resolve_render_task_id();
+                                let backdrop_surface_id = backdrop_picture.raster_config
+                                    .as_ref()
+                                    .unwrap()
+                                    .surface_index;
+                                let backdrop_id = ctx.surfaces[backdrop_surface_id.0]
+                                    .surface
+                                    .as_ref()
+                                    .expect("bug: backdrop surface must be allocated by now")
+                                    .resolve_render_task_id();
 
                                 let key = BatchKey::new(
                                     BatchKind::Brush(
                                         BrushBatchKind::MixBlend {
                                             task_id,
-                                            source_id: cache_task_id,
+                                            source_id,
                                             backdrop_id,
                                         },
                                     ),
-                                    BlendMode::PremultipliedAlpha,
+                                    non_segmented_blend_mode,
                                     BatchTextures::no_texture(),
                                 );
+
+                                // The trick here is to draw the picture in the space of the backdrop,
+                                // since the source can be attached to a child spatial node.
+                                let expanded_header = PrimitiveHeader {
+                                    local_rect: ctx.clip_scroll_tree
+                                        .map_rect_to_parent_space(
+                                            prim_header.local_rect,
+                                            picture.spatial_node_index,
+                                            backdrop_picture.spatial_node_index,
+                                            &backdrop_picture.local_rect, //Note: this shouldn't be used
+                                        )
+                                        .unwrap_or_else(LayoutRect::zero)
+                                        .union(&backdrop_picture.local_rect),
+                                    local_clip_rect: ctx.clip_scroll_tree
+                                        .map_rect_to_parent_space(
+                                            prim_header.local_clip_rect,
+                                            picture.spatial_node_index,
+                                            backdrop_picture.spatial_node_index,
+                                            &backdrop_picture.local_clip_rect, //Note: this shouldn't be used
+                                        )
+                                        .unwrap_or_else(LayoutRect::zero)
+                                        .union(&backdrop_picture.local_clip_rect),
+                                    transform_id: transforms
+                                        .get_id(
+                                            backdrop_picture.spatial_node_index,
+                                            root_spatial_node_index,
+                                            ctx.clip_scroll_tree,
+                                        ),
+                                    ..prim_header
+                                };
+
                                 let backdrop_task_address = render_tasks.get_task_address(backdrop_id);
-                                let source_task_address = render_tasks.get_task_address(cache_task_id);
-                                let prim_header_index = prim_headers.push(&prim_header, z_id, [
+                                let source_task_address = render_tasks.get_task_address(source_id);
+                                let prim_header_index = prim_headers.push(&expanded_header, z_id, [
                                     mode as u32 as i32,
                                     backdrop_task_address.0 as i32,
                                     source_task_address.0 as i32,
                                 ]);
 
                                 let instance = BrushInstance {
                                     prim_header_index,
                                     clip_task_address,
                                     segment_index: INVALID_SEGMENT_INDEX,
                                     edge_flags: EdgeAaSegmentMask::empty(),
                                     brush_flags,
                                     user_data: 0,
                                 };
+                                //TODO: investigate if we can do better. We can't use the `bounding_rect`
+                                // here because we effectively merge the call with the backdrop,
+                                // and the instance for the backdrop isn't available here.
+                                let conservative_bounding_rect = PictureRect::max_rect();
 
                                 self.current_batch_list().push_single_instance(
                                     key,
-                                    bounding_rect,
+                                    &conservative_bounding_rect,
                                     z_id,
                                     PrimitiveInstanceData::from(instance),
                                 );
                             }
+                            PictureCompositeMode::Puppet { .. } |
+                            PictureCompositeMode::MixBlend { .. } |
                             PictureCompositeMode::Blit(_) => {
                                 let surface = ctx.surfaces[raster_config.surface_index.0]
                                     .surface
                                     .as_ref()
                                     .expect("bug: surface must be allocated by now");
                                 let cache_task_id = surface.resolve_render_task_id();
                                 let kind = BatchKind::Brush(
                                     BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
--- a/gfx/wr/webrender/src/clip_scroll_tree.rs
+++ b/gfx/wr/webrender/src/clip_scroll_tree.rs
@@ -6,17 +6,17 @@ use api::{ExternalScrollId, LayoutPoint,
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::{LayoutSize, LayoutTransform, PropertyBinding, TransformStyle, WorldPoint};
 use gpu_types::TransformPalette;
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use scene::SceneProperties;
 use spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
 use std::ops;
-use util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
+use util::{project_rect, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
 
 pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
 
 /// An id that identifies coordinate systems in the ClipScrollTree. Each
 /// coordinate system has an id and those ids will be shared when the coordinates
 /// system are the same or are in the same axis-aligned space. This allows
 /// for optimizing mask generation.
 #[derive(Debug, Copy, Clone, PartialEq)]
@@ -206,16 +206,45 @@ impl ClipScrollTree {
 
         Some(RelativeTransform {
             flattened: transform,
             visible_face,
             is_perspective,
         })
     }
 
+    /// Map a rectangle in some child space to a parent.
+    /// Doesn't handle preserve-3d islands.
+    pub fn map_rect_to_parent_space(
+        &self,
+        mut rect: LayoutRect,
+        child_index: SpatialNodeIndex,
+        parent_index: SpatialNodeIndex,
+        parent_bounds: &LayoutRect,
+    ) -> Option<LayoutRect> {
+        if child_index == parent_index {
+            return Some(rect);
+        }
+        assert!(child_index.0 > parent_index.0);
+
+        let child = &self.spatial_nodes[child_index.0 as usize];
+        let parent = &self.spatial_nodes[parent_index.0 as usize];
+
+        let mut coordinate_system_id = child.coordinate_system_id;
+        rect = child.coordinate_system_relative_scale_offset.map_rect(&rect);
+
+        while coordinate_system_id != parent.coordinate_system_id {
+            let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
+            coordinate_system_id = coord_system.parent.expect("invalid parent!");
+            rect = project_rect(&coord_system.transform, &rect, parent_bounds)?;
+        }
+
+        Some(parent.coordinate_system_relative_scale_offset.unmap_rect(&rect))
+    }
+
     /// Returns true if the spatial node is the same as the parent, or is
     /// a child of the parent.
     pub fn is_same_or_child_of(
         &self,
         spatial_node_index: SpatialNodeIndex,
         parent_spatial_node_index: SpatialNodeIndex,
     ) -> bool {
         let mut index = spatial_node_index;
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -17,17 +17,17 @@ use clip::{ClipChainId, ClipRegion, Clip
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
 use hit_test::{HitTestingItem, HitTestingRun};
 use image::simplify_repeated_primitive;
 use intern::{Handle, Internable, InternDebug};
 use internal_types::{FastHashMap, FastHashSet};
 use picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
-use picture::{BlitReason, PrimitiveList, TileCache};
+use picture::{BlitReason, OrderedPictureChild, PrimitiveList, TileCache};
 use prim_store::{PrimitiveInstance, PrimitiveKeyKind, PrimitiveSceneData};
 use prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
 use prim_store::{PrimitiveStoreStats, ScrollNodeAndClipChain, PictureIndex};
 use prim_store::{register_prim_chase_id, get_line_decoration_sizes};
 use prim_store::borders::{ImageBorder, NormalBorderPrim};
 use prim_store::gradient::{GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams};
 use prim_store::image::{Image, YuvImage};
 use prim_store::line_dec::{LineDecoration, LineDecorationCacheKey};
@@ -1253,33 +1253,54 @@ impl<'a> DisplayListFlattener<'a> {
         let is_pipeline_root =
             self.sc_stack.last().map_or(true, |sc| sc.pipeline_id != pipeline_id);
         let frame_output_pipeline_id = if is_pipeline_root && self.output_pipelines.contains(&pipeline_id) {
             Some(pipeline_id)
         } else {
             None
         };
 
-        // Get the transform-style of the parent stacking context,
+        // Figure out if the parent is in 3D context,
         // which determines if we *might* need to draw this on
         // an intermediate surface for plane splitting purposes.
-        let (parent_is_3d, extra_3d_instance) = match self.sc_stack.last_mut() {
-            Some(sc) => {
+        let (parent_is_3d, extra_3d_picture, backdrop_picture) = match self.sc_stack.last_mut() {
+            Some(ref mut sc) if composite_ops.mix_blend_mode.is_some() => {
+                // Cut the sequence of children before starting a mix-blend stacking context,
+                // so that we have a source picture for applying the blending operator.
+                let backdrop_picture = sc.cut_item_sequence(
+                    &mut self.prim_store,
+                    &mut self.interners,
+                    PictureCompositeMode::Puppet { master: None },
+                    Picture3DContext::Out,
+                );
+                (false, None, backdrop_picture)
+            }
+            Some(ref mut sc) if sc.is_3d() => {
+                let flat_items_context_3d = match sc.context_3d {
+                    Picture3DContext::In { ancestor_index, .. } => Picture3DContext::In {
+                        root_data: None,
+                        ancestor_index,
+                    },
+                    Picture3DContext::Out => panic!("Unexpected out of 3D context"),
+                };
                 // Cut the sequence of flat children before starting a child stacking context,
                 // so that the relative order between them and our current SC is preserved.
-                let extra_instance = sc.cut_flat_item_sequence(
+                let extra_picture = sc.cut_item_sequence(
                     &mut self.prim_store,
                     &mut self.interners,
+                    PictureCompositeMode::Blit(BlitReason::PRESERVE3D),
+                    flat_items_context_3d,
                 );
-                (sc.is_3d(), extra_instance)
-            },
-            None => (false, None),
+
+                (true, extra_picture, None)
+            }
+            Some(_) | None => (false, None, None),
         };
 
-        if let Some(instance) = extra_3d_instance {
+        if let Some((_picture_index, instance)) = extra_3d_picture {
             self.add_primitive_instance_to_3d_root(instance);
         }
 
         // If this is preserve-3d *or* the parent is, then this stacking
         // context is participating in the 3d rendering context. In that
         // case, hoist the picture up to the 3d rendering context
         // container, so that it's rendered as a sibling with other
         // elements in this context.
@@ -1307,33 +1328,36 @@ impl<'a> DisplayListFlattener<'a> {
         } else {
             Picture3DContext::Out
         };
 
         // 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
         // clip node doesn't affect the stacking context rect.
-        let blit_reason = if clip_chain_id == ClipChainId::NONE {
-            BlitReason::empty()
-        } else {
-            BlitReason::CLIP
-        };
+        let mut blit_reason = BlitReason::empty();
+        if clip_chain_id != ClipChainId::NONE {
+            blit_reason |= BlitReason::CLIP
+        }
+        if participating_in_3d_context {
+            blit_reason |= BlitReason::PRESERVE3D;
+        }
 
         // Push the SC onto the stack, so we know how to handle things in
         // pop_stacking_context.
         self.sc_stack.push(FlattenedStackingContext {
             primitives: Vec::new(),
             pipeline_id,
             is_backface_visible,
             requested_raster_space,
             spatial_node_index,
             clip_chain_id,
             frame_output_pipeline_id,
             composite_ops,
+            backdrop_picture,
             blit_reason,
             transform_style,
             context_3d,
             create_tile_cache,
         });
     }
 
     pub fn pop_stacking_context(&mut self) {
@@ -1343,92 +1367,86 @@ impl<'a> DisplayListFlattener<'a> {
         // of creating a picture, just append the primitive list to the parent stacking
         // context as a short cut. This serves two purposes:
         // (a) It's an optimization to reduce picture count and allocations, as display lists
         //     often contain a lot of these stacking contexts that don't require pictures or
         //     off-screen surfaces.
         // (b) It's useful for the initial version of picture caching in gecko, by enabling
         //     is to just look for interesting scroll roots on the root stacking context,
         //     without having to consider cuts at stacking context boundaries.
-        let parent_is_empty = match self.sc_stack.last_mut() {
-            Some(parent_sc) => {
-                if stacking_context.is_redundant(
-                    parent_sc,
-                    self.clip_scroll_tree,
-                ) {
-                    // If the parent context primitives list is empty, it's faster
-                    // to assign the storage of the popped context instead of paying
-                    // the copying cost for extend.
-                    if parent_sc.primitives.is_empty() {
-                        parent_sc.primitives = stacking_context.primitives;
-                    } else {
-                        parent_sc.primitives.extend(stacking_context.primitives);
-                    }
-                    return;
+        if let Some(parent_sc) = self.sc_stack.last_mut() {
+            if stacking_context.is_redundant(
+                parent_sc,
+                self.clip_scroll_tree,
+            ) {
+                // If the parent context primitives list is empty, it's faster
+                // to assign the storage of the popped context instead of paying
+                // the copying cost for extend.
+                if parent_sc.primitives.is_empty() {
+                    parent_sc.primitives = stacking_context.primitives;
+                } else {
+                    parent_sc.primitives.extend(stacking_context.primitives);
                 }
-                parent_sc.primitives.is_empty()
-            },
-            None => true,
-        };
+                return;
+            }
+        }
 
         if stacking_context.create_tile_cache {
             self.setup_picture_caching(
                 &mut stacking_context.primitives,
             );
         }
 
         // 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();
 
-        let (leaf_context_3d, leaf_composite_mode, leaf_output_pipeline_id) = match stacking_context.context_3d {
+        let leaf_composite_mode = if stacking_context.blit_reason.is_empty() {
+            // By default, this picture will be collapsed into
+            // the owning target.
+            None
+        } else {
+            // Add a dummy composite filter if the SC has to be isolated.
+            Some(PictureCompositeMode::Blit(stacking_context.blit_reason))
+        };
+
+        let leaf_context_3d = match stacking_context.context_3d {
             // TODO(gw): For now, as soon as this picture is in
             //           a 3D context, we draw it to an intermediate
             //           surface and apply plane splitting. However,
             //           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.
-            Picture3DContext::In { ancestor_index, .. } => (
-                Picture3DContext::In { root_data: None, ancestor_index },
-                Some(PictureCompositeMode::Blit(BlitReason::PRESERVE3D | stacking_context.blit_reason)),
-                None,
-            ),
-            Picture3DContext::Out => (
-                Picture3DContext::Out,
-                if stacking_context.blit_reason.is_empty() {
-                    // By default, this picture will be collapsed into
-                    // the owning target.
-                    None
-                } else {
-                    // Add a dummy composite filter if the SC has to be isolated.
-                    Some(PictureCompositeMode::Blit(stacking_context.blit_reason))
-                },
-                stacking_context.frame_output_pipeline_id
-            ),
+            Picture3DContext::In { ancestor_index, .. } => {
+                assert_ne!(leaf_composite_mode, None);
+                Picture3DContext::In { root_data: None, ancestor_index }
+            }
+            Picture3DContext::Out => Picture3DContext::Out,
         };
 
+        let leaf_prim_list = PrimitiveList::new(
+            stacking_context.primitives,
+            &self.interners,
+        );
         // Add picture for this actual stacking context contents to render into.
         let leaf_pic_index = PictureIndex(self.prim_store.pictures
             .alloc()
             .init(PicturePrimitive::new_image(
                 leaf_composite_mode,
                 leaf_context_3d,
                 stacking_context.pipeline_id,
-                leaf_output_pipeline_id,
+                stacking_context.frame_output_pipeline_id,
                 true,
                 stacking_context.requested_raster_space,
-                PrimitiveList::new(
-                    stacking_context.primitives,
-                    &self.interners,
-                ),
+                leaf_prim_list,
                 stacking_context.spatial_node_index,
                 max_clip,
                 None,
                 PictureOptions::default(),
             ))
         );
 
         // Create a chain of pictures based on presence of filters,
@@ -1527,30 +1545,36 @@ impl<'a> DisplayListFlattener<'a> {
                 println!("\tis a composite picture for a stacking context with {:?}", filter);
             }
 
             // Run the optimize pass on this picture, to see if we can
             // collapse opacity and avoid drawing to an off-screen surface.
             self.prim_store.optimize_picture_if_possible(current_pic_index);
         }
 
-        // Same for mix-blend-mode, except we can skip if this primitive is the first in the parent
-        // stacking context.
+        // Same for mix-blend-mode, except we can skip if the backdrop doesn't have any primitives.
         // From https://drafts.fxtf.org/compositing-1/#generalformula, the formula for blending is:
         // Cs = (1 - ab) x Cs + ab x Blend(Cb, Cs)
         // where
         // Cs = Source color
         // ab = Backdrop alpha
         // Cb = Backdrop color
         //
         // If we're the first primitive within a stacking context, then we can guarantee that the
         // backdrop alpha will be 0, and then the blend equation collapses to just
         // Cs = Cs, and the blend mode isn't taken into account at all.
-        let has_mix_blend = if let (Some(mix_blend_mode), false) = (stacking_context.composite_ops.mix_blend_mode, parent_is_empty) {
-            let composite_mode = Some(PictureCompositeMode::MixBlend(mix_blend_mode));
+        if let (Some(mode), Some((backdrop, backdrop_instance))) = (stacking_context.composite_ops.mix_blend_mode, stacking_context.backdrop_picture.take()) {
+            let composite_mode = Some(PictureCompositeMode::MixBlend { mode, backdrop });
+
+            // We need to make the backdrop picture to be at the same level as the content,
+            // to be available as a source for composition...
+            if let Some(parent_sc) = self.sc_stack.last_mut() {
+                // Not actually rendered, due to `PictureCompositeMode::Puppet`, unless the blend picture is culled.
+                parent_sc.primitives.push(backdrop_instance);
+            }
 
             let blend_pic_index = PictureIndex(self.prim_store.pictures
                 .alloc()
                 .init(PicturePrimitive::new_image(
                     composite_mode,
                     Picture3DContext::Out,
                     stacking_context.pipeline_id,
                     None,
@@ -1562,54 +1586,49 @@ impl<'a> DisplayListFlattener<'a> {
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     None,
                     PictureOptions::default(),
                 ))
             );
 
+            // Assoiate the backdrop picture with the blend.
+            self.prim_store.pictures[backdrop.0].requested_composite_mode = Some(PictureCompositeMode::Puppet {
+                master: Some(blend_pic_index),
+            });
+
             current_pic_index = blend_pic_index;
             cur_instance = create_prim_instance(
-                blend_pic_index,
+                current_pic_index,
                 composite_mode.into(),
                 stacking_context.is_backface_visible,
                 ClipChainId::NONE,
                 stacking_context.spatial_node_index,
                 &mut self.interners,
             );
 
             if cur_instance.is_chased() {
-                println!("\tis a mix-blend picture for a stacking context with {:?}", mix_blend_mode);
+                println!("\tis a mix-blend picture for a stacking context with {:?}", mode);
             }
-            true
-        } else {
-            false
-        };
+        }
 
         // Set the stacking context clip on the outermost picture in the chain,
         // unless we already set it on the leaf picture.
         cur_instance.clip_chain_id = stacking_context.clip_chain_id;
 
         // The primitive instance for the remainder of flat children of this SC
         // if it's a part of 3D hierarchy but not the root of it.
         let trailing_children_instance = match self.sc_stack.last_mut() {
             // Preserve3D path (only relevant if there are no filters/mix-blend modes)
             Some(ref parent_sc) if parent_sc.is_3d() => {
                 Some(cur_instance)
             }
             // Regular parenting path
             Some(ref mut parent_sc) => {
-                // If we have a mix-blend-mode, 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 has_mix_blend {
-                    parent_sc.blit_reason |= BlitReason::ISOLATE;
-                }
                 parent_sc.primitives.push(cur_instance);
                 None
             }
             // This must be the root stacking context
             None => {
                 self.root_pic_index = current_pic_index;
                 None
             }
@@ -2626,16 +2645,19 @@ struct FlattenedStackingContext {
     /// If set, this should be provided to caller
     /// as an output texture.
     frame_output_pipeline_id: Option<PipelineId>,
 
     /// The list of filters / mix-blend-mode for this
     /// stacking context.
     composite_ops: CompositeOps,
 
+    /// For a mix-blend stacking context, specify the picture index for backdrop.
+    backdrop_picture: Option<(PictureIndex, PrimitiveInstance)>,
+
     /// Bitfield of reasons this stacking context needs to
     /// be an offscreen surface.
     blit_reason: BlitReason,
 
     /// Pipeline this stacking context belongs to.
     pipeline_id: PipelineId,
 
     /// CSS transform-style property.
@@ -2668,17 +2690,18 @@ impl FlattenedStackingContext {
         // If there are filters / mix-blend-mode
         if !self.composite_ops.filters.is_empty() {
             return false;
         }
 
         // We can skip mix-blend modes if they are the first primitive in a stacking context,
         // see pop_stacking_context for a full explanation.
         if !self.composite_ops.mix_blend_mode.is_none() &&
-           !parent.primitives.is_empty() {
+           !self.backdrop_picture.is_none()
+        {
             return false;
         }
 
         // If backface visibility is different
         if self.is_backface_visible != parent.is_backface_visible {
             return false;
         }
 
@@ -2706,39 +2729,34 @@ impl FlattenedStackingContext {
         if self.create_tile_cache {
             return false;
         }
 
         // It is redundant!
         true
     }
 
-    /// For a Preserve3D context, cut the sequence of the immediate flat children
+    /// Cut the sequence of the immediate children of a stacking context
     /// recorded so far and generate a picture from them.
-    pub fn cut_flat_item_sequence(
+    fn cut_item_sequence(
         &mut self,
         prim_store: &mut PrimitiveStore,
         interners: &mut Interners,
-    ) -> Option<PrimitiveInstance> {
-        if !self.is_3d() || self.primitives.is_empty() {
+        composite_mode: PictureCompositeMode,
+        context_3d: Picture3DContext<OrderedPictureChild>,
+    ) -> Option<(PictureIndex, PrimitiveInstance)> {
+        if self.primitives.is_empty() {
             return None
         }
-        let flat_items_context_3d = match self.context_3d {
-            Picture3DContext::In { ancestor_index, .. } => Picture3DContext::In {
-                root_data: None,
-                ancestor_index,
-            },
-            Picture3DContext::Out => panic!("Unexpected out of 3D context"),
-        };
 
         let pic_index = PictureIndex(prim_store.pictures
             .alloc()
             .init(PicturePrimitive::new_image(
-                Some(PictureCompositeMode::Blit(BlitReason::PRESERVE3D)),
-                flat_items_context_3d,
+                Some(composite_mode),
+                context_3d,
                 self.pipeline_id,
                 None,
                 true,
                 self.requested_raster_space,
                 PrimitiveList::new(
                     mem::replace(&mut self.primitives, Vec::new()),
                     interners,
                 ),
@@ -2753,17 +2771,17 @@ impl FlattenedStackingContext {
             pic_index,
             PictureCompositeKey::Identity,
             self.is_backface_visible,
             self.clip_chain_id,
             self.spatial_node_index,
             interners,
         );
 
-        Some(prim_instance)
+        Some((pic_index, prim_instance))
     }
 }
 
 /// A primitive that is added while a shadow context is
 /// active is stored as a pending primitive and only
 /// added to pictures during pop_all_shadows.
 pub struct PendingPrimitive<T> {
     clip_and_scroll: ScrollNodeAndClipChain,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1865,33 +1865,43 @@ pub struct RasterConfig {
     /// Whether this picture establishes a rasterization root.
     pub establishes_raster_root: bool,
 }
 
 bitflags! {
     /// A set of flags describing why a picture may need a backing surface.
     #[cfg_attr(feature = "capture", derive(Serialize))]
     pub struct BlitReason: u32 {
-        /// Mix-blend-mode on a child that requires isolation.
-        const ISOLATE = 1;
         /// Clip node that _might_ require a surface.
-        const CLIP = 2;
+        const CLIP = 1;
         /// Preserve-3D requires a surface for plane-splitting.
-        const PRESERVE3D = 4;
+        const PRESERVE3D = 2;
     }
 }
 
 /// Specifies how this Picture should be composited
 /// onto the target it belongs to.
 #[allow(dead_code)]
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub enum PictureCompositeMode {
+    /// Don't composite this picture in a standard way,
+    /// can be used for pictures that need to be isolated but used
+    /// manually, e.g. for the backdrop of mix-blend pictures.
+    Puppet {
+        /// The master picture that actually handles compositing
+        /// of this one. If that picture turns out to be invisible,
+        /// the puppet mode becomes a regular blit.
+        master: Option<PictureIndex>,
+    },
     /// Apply CSS mix-blend-mode effect.
-    MixBlend(MixBlendMode),
+    MixBlend {
+        mode: MixBlendMode,
+        backdrop: PictureIndex,
+    },
     /// Apply a CSS filter.
     Filter(FilterOp),
     /// 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,
@@ -3055,47 +3065,16 @@ impl PicturePrimitive {
 
                     // segment rect / extra data
                     request.push(shadow_rect);
                     request.push([0.0, 0.0, 0.0, 0.0]);
                 }
 
                 PictureSurface::RenderTask(render_task_id)
             }
-            PictureCompositeMode::MixBlend(..) => {
-                let uv_rect_kind = calculate_uv_rect_kind(
-                    &pic_rect,
-                    &transform,
-                    &clipped,
-                    device_pixel_scale,
-                    true,
-                );
-
-                let picture_task = RenderTask::new_picture(
-                    RenderTaskLocation::Dynamic(None, clipped.size),
-                    unclipped.size,
-                    pic_index,
-                    clipped.origin,
-                    child_tasks,
-                    uv_rect_kind,
-                    pic_context.raster_spatial_node_index,
-                    device_pixel_scale,
-                );
-
-                let readback_task_id = frame_state.render_tasks.add(
-                    RenderTask::new_readback(clipped)
-                );
-
-                self.secondary_render_task_id = Some(readback_task_id);
-                surfaces[surface_index.0].tasks.push(readback_task_id);
-
-                let render_task_id = frame_state.render_tasks.add(picture_task);
-                surfaces[surface_index.0].tasks.push(render_task_id);
-                PictureSurface::RenderTask(render_task_id)
-            }
             PictureCompositeMode::Filter(filter) => {
                 if let FilterOp::ColorMatrix(m) = filter {
                     if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handle) {
                         for i in 0..5 {
                             request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
                         }
                     }
                 }
@@ -3118,21 +3097,23 @@ impl PicturePrimitive {
                     pic_context.raster_spatial_node_index,
                     device_pixel_scale,
                 );
 
                 let render_task_id = frame_state.render_tasks.add(picture_task);
                 surfaces[surface_index.0].tasks.push(render_task_id);
                 PictureSurface::RenderTask(render_task_id)
             }
+            PictureCompositeMode::Puppet { .. } |
+            PictureCompositeMode::MixBlend { .. } |
             PictureCompositeMode::Blit(_) => {
                 // The SplitComposite shader used for 3d contexts doesn't snap
                 // to pixels, so we shouldn't snap our uv coordinates either.
                 let supports_snapping = match self.context_3d {
-                    Picture3DContext::In{ .. } => false,
+                    Picture3DContext::In { .. } => false,
                     _ => true,
                 };
 
                 let uv_rect_kind = calculate_uv_rect_kind(
                     &pic_rect,
                     &transform,
                     &clipped,
                     device_pixel_scale,
--- a/gfx/wr/webrender/src/prim_store/picture.rs
+++ b/gfx/wr/webrender/src/prim_store/picture.rs
@@ -57,17 +57,17 @@ pub enum PictureCompositeKey {
     Saturation,
     Color,
     Luminosity,
 }
 
 impl From<Option<PictureCompositeMode>> for PictureCompositeKey {
     fn from(mode: Option<PictureCompositeMode>) -> Self {
         match mode {
-            Some(PictureCompositeMode::MixBlend(mode)) => {
+            Some(PictureCompositeMode::MixBlend { mode, .. }) => {
                 match mode {
                     MixBlendMode::Normal => PictureCompositeKey::Identity,
                     MixBlendMode::Multiply => PictureCompositeKey::Multiply,
                     MixBlendMode::Screen => PictureCompositeKey::Screen,
                     MixBlendMode::Overlay => PictureCompositeKey::Overlay,
                     MixBlendMode::Darken => PictureCompositeKey::Darken,
                     MixBlendMode::Lighten => PictureCompositeKey::Lighten,
                     MixBlendMode::ColorDodge => PictureCompositeKey::ColorDodge,
@@ -112,16 +112,17 @@ impl From<Option<PictureCompositeMode>> 
                         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)
                     }
                 }
             }
+            Some(PictureCompositeMode::Puppet { .. }) |
             Some(PictureCompositeMode::Blit(_)) |
             Some(PictureCompositeMode::TileCache { .. }) |
             None => {
                 PictureCompositeKey::Identity
             }
         }
     }
 }
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -402,17 +402,16 @@ pub struct RenderTaskData {
 pub enum RenderTaskKind {
     Picture(PictureTask),
     CacheMask(CacheMaskTask),
     ClipRegion(ClipRegionTask),
     VerticalBlur(BlurTask),
     HorizontalBlur(BlurTask),
     #[allow(dead_code)]
     Glyph(GlyphTask),
-    Readback(DeviceIntRect),
     Scaling(ScalingTask),
     Blit(BlitTask),
     Border(BorderTask),
     LineDecoration(LineDecorationTask),
 }
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -491,25 +490,16 @@ impl RenderTask {
                 root_spatial_node_index,
                 device_pixel_scale,
             }),
             clear_mode: ClearMode::Transparent,
             saved_index: None,
         }
     }
 
-    pub fn new_readback(screen_rect: DeviceIntRect) -> Self {
-        RenderTask::with_dynamic_location(
-            screen_rect.size,
-            Vec::new(),
-            RenderTaskKind::Readback(screen_rect),
-            ClearMode::Transparent,
-        )
-    }
-
     pub fn new_blit(
         size: DeviceIntSize,
         source: BlitSource,
     ) -> Self {
         RenderTask::new_blit_with_padding(size, &DeviceIntSideOffsets::zero(), source)
     }
 
     pub fn new_blit_with_padding(
@@ -849,18 +839,17 @@ impl RenderTask {
             }),
             clear_mode: ClearMode::Transparent,
             saved_index: None,
         }
     }
 
     fn uv_rect_kind(&self) -> UvRectKind {
         match self.kind {
-            RenderTaskKind::CacheMask(..) |
-            RenderTaskKind::Readback(..) => {
+            RenderTaskKind::CacheMask(..) => {
                 unreachable!("bug: unexpected render task");
             }
 
             RenderTaskKind::Picture(ref task) => {
                 task.uv_rect_kind
             }
 
             RenderTaskKind::VerticalBlur(ref task) |
@@ -923,17 +912,16 @@ impl RenderTask {
                     task.blur_std_deviation,
                     0.0,
                     0.0,
                 ]
             }
             RenderTaskKind::Glyph(_) => {
                 [0.0, 1.0, 0.0]
             }
-            RenderTaskKind::Readback(..) |
             RenderTaskKind::Scaling(..) |
             RenderTaskKind::Border(..) |
             RenderTaskKind::LineDecoration(..) |
             RenderTaskKind::Blit(..) => {
                 [0.0; 3]
             }
         };
 
@@ -964,17 +952,16 @@ impl RenderTask {
             RenderTaskKind::Picture(ref info) => {
                 gpu_cache.get_address(&info.uv_rect_handle)
             }
             RenderTaskKind::VerticalBlur(ref info) |
             RenderTaskKind::HorizontalBlur(ref info) => {
                 gpu_cache.get_address(&info.uv_rect_handle)
             }
             RenderTaskKind::ClipRegion(..) |
-            RenderTaskKind::Readback(..) |
             RenderTaskKind::Scaling(..) |
             RenderTaskKind::Blit(..) |
             RenderTaskKind::Border(..) |
             RenderTaskKind::CacheMask(..) |
             RenderTaskKind::LineDecoration(..) |
             RenderTaskKind::Glyph(..) => {
                 panic!("texture handle not supported for this task kind");
             }
@@ -1017,18 +1004,16 @@ impl RenderTask {
             RenderTaskLocation::TextureCache {layer, rect, .. } => {
                 (rect, RenderTargetIndex(layer as usize))
             }
         }
     }
 
     pub fn target_kind(&self) -> RenderTargetKind {
         match self.kind {
-            RenderTaskKind::Readback(..) => RenderTargetKind::Color,
-
             RenderTaskKind::LineDecoration(..) => RenderTargetKind::Color,
 
             RenderTaskKind::ClipRegion(..) |
             RenderTaskKind::CacheMask(..) => {
                 RenderTargetKind::Alpha
             }
 
             RenderTaskKind::VerticalBlur(ref task_info) |
@@ -1071,17 +1056,16 @@ impl RenderTask {
         let (cache_handle, uv_rect_kind) = match self.kind {
             RenderTaskKind::HorizontalBlur(ref mut info) |
             RenderTaskKind::VerticalBlur(ref mut info) => {
                 (&mut info.uv_rect_handle, info.uv_rect_kind)
             }
             RenderTaskKind::Picture(ref mut info) => {
                 (&mut info.uv_rect_handle, info.uv_rect_kind)
             }
-            RenderTaskKind::Readback(..) |
             RenderTaskKind::Scaling(..) |
             RenderTaskKind::Blit(..) |
             RenderTaskKind::ClipRegion(..) |
             RenderTaskKind::Border(..) |
             RenderTaskKind::CacheMask(..) |
             RenderTaskKind::LineDecoration(..) |
             RenderTaskKind::Glyph(..) => {
                 return;
@@ -1122,20 +1106,16 @@ impl RenderTask {
             RenderTaskKind::VerticalBlur(ref task) => {
                 pt.new_level("VerticalBlur".to_owned());
                 task.print_with(pt);
             }
             RenderTaskKind::HorizontalBlur(ref task) => {
                 pt.new_level("HorizontalBlur".to_owned());
                 task.print_with(pt);
             }
-            RenderTaskKind::Readback(ref rect) => {
-                pt.new_level("Readback".to_owned());
-                pt.add_item(format!("rect: {:?}", rect));
-            }
             RenderTaskKind::Scaling(ref kind) => {
                 pt.new_level("Scaling".to_owned());
                 pt.add_item(format!("kind: {:?}", kind));
             }
             RenderTaskKind::Border(..) => {
                 pt.new_level("Border".to_owned());
             }
             RenderTaskKind::Blit(ref task) => {
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -65,17 +65,17 @@ use profiler::{BackendProfileCounters, F
 use profiler::{Profiler, ChangeIndicator};
 use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
 use render_backend::{FrameId, RenderBackend};
 use scene_builder::{SceneBuilder, LowPrioritySceneBuilder};
 use shade::{Shaders, WrShaders};
 use smallvec::SmallVec;
-use render_task::{RenderTask, RenderTaskKind, RenderTaskTree};
+use render_task::RenderTaskTree;
 use resource_cache::ResourceCache;
 use util::drain_filter;
 
 use std;
 use std::cmp;
 use std::collections::VecDeque;
 use std::collections::hash_map::Entry;
 use std::f32;
@@ -2308,21 +2308,16 @@ impl Renderer {
 
         debug_target.add(
             debug_server::BatchKind::Cache,
             "Scalings",
             target.scalings.len(),
         );
         debug_target.add(
             debug_server::BatchKind::Cache,
-            "Readbacks",
-            target.readbacks.len(),
-        );
-        debug_target.add(
-            debug_server::BatchKind::Cache,
             "Vertical Blur",
             target.vertical_blurs.len(),
         );
         debug_target.add(
             debug_server::BatchKind::Cache,
             "Horizontal Blur",
             target.horizontal_blurs.len(),
         );
@@ -3022,84 +3017,16 @@ impl Renderer {
                 self.profile_counters.draw_calls.inc();
                 stats.total_draw_calls += 1;
             }
         }
 
         self.profile_counters.vertices.add(6 * data.len());
     }
 
-    fn handle_readback_composite(
-        &mut self,
-        draw_target: DrawTarget,
-        uses_scissor: bool,
-        source: &RenderTask,
-        backdrop: &RenderTask,
-        readback: &RenderTask,
-    ) {
-        if uses_scissor {
-            self.device.disable_scissor();
-        }
-
-        let cache_texture = self.texture_resolver
-            .resolve(&TextureSource::PrevPassColor)
-            .unwrap();
-
-        // Before submitting the composite batch, do the
-        // framebuffer readbacks that are needed for each
-        // composite operation in this batch.
-        let (readback_rect, readback_layer) = readback.get_target_rect();
-        let (backdrop_rect, _) = backdrop.get_target_rect();
-        let backdrop_screen_origin = match backdrop.kind {
-            RenderTaskKind::Picture(ref task_info) => task_info.content_origin,
-            _ => panic!("bug: composite on non-picture?"),
-        };
-        let source_screen_origin = match source.kind {
-            RenderTaskKind::Picture(ref task_info) => task_info.content_origin,
-            _ => panic!("bug: composite on non-picture?"),
-        };
-
-        // Bind the FBO to blit the backdrop to.
-        // Called per-instance in case the layer (and therefore FBO)
-        // changes. The device will skip the GL call if the requested
-        // target is already bound.
-        let cache_draw_target = DrawTarget::Texture {
-            texture: cache_texture,
-            layer: readback_layer.0 as usize,
-            with_depth: false,
-        };
-        self.device.bind_draw_target(cache_draw_target);
-
-        let mut src = DeviceIntRect::new(
-            source_screen_origin + (backdrop_rect.origin - backdrop_screen_origin),
-            readback_rect.size,
-        );
-        let mut dest = readback_rect.to_i32();
-
-        // Need to invert the y coordinates and flip the image vertically when
-        // reading back from the framebuffer.
-        if draw_target.is_default() {
-            src.origin.y = draw_target.dimensions().height as i32 - src.size.height - src.origin.y;
-            dest.origin.y += dest.size.height;
-            dest.size.height = -dest.size.height;
-        }
-
-        self.device.bind_read_target(draw_target.into());
-        self.device.blit_render_target(src, dest, TextureFilter::Linear);
-
-        // Restore draw target to current pass render target + layer, and reset
-        // the read target.
-        self.device.bind_draw_target(draw_target);
-        self.device.reset_read_target();
-
-        if uses_scissor {
-            self.device.enable_scissor();
-        }
-    }
-
     fn handle_blits(
         &mut self,
         blits: &[BlitJob],
         render_tasks: &RenderTaskTree,
     ) {
         if blits.is_empty() {
             return;
         }
@@ -3415,30 +3342,16 @@ impl Renderer {
                                 //
                                 self.device.set_blend_mode_subpixel_with_bg_color_pass0();
                                 self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
                             }
                         }
                         prev_blend_mode = batch.key.blend_mode;
                     }
 
-                    // Handle special case readback for composites.
-                    if let BatchKind::Brush(BrushBatchKind::MixBlend { task_id, source_id, backdrop_id }) = batch.key.kind {
-                        // composites can't be grouped together because
-                        // they may overlap and affect each other.
-                        debug_assert_eq!(batch.instances.len(), 1);
-                        self.handle_readback_composite(
-                            draw_target,
-                            uses_scissor,
-                            &render_tasks[source_id],
-                            &render_tasks[task_id],
-                            &render_tasks[backdrop_id],
-                        );
-                    }
-
                     let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
 
                     iterate_regions(
                         &alpha_batch_container.regions,
                         |region| {
                             if let Some(region) = region {
                                 let scissor_rect = draw_target.build_scissor_rect(
                                     Some(region),
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -15,17 +15,17 @@ use euclid::{TypedPoint2D, TypedVector2D
 use frame_builder::FrameGlobalResources;
 use gpu_cache::{GpuCache};
 use gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
 use gpu_types::{TransformData, TransformPalette, ZBufferIdGenerator};
 use internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use picture::{RecordedDirtyRegion, SurfaceInfo};
-use prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
+use prim_store::{PictureIndex, PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
 use profiler::FrameProfileCounters;
 use render_backend::{DataStores, FrameId};
 use render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind};
 use render_task::{BlurTask, ClearMode, GlyphTask, RenderTaskLocation, RenderTaskTree, ScalingTask};
 use resource_cache::ResourceCache;
 use std::{cmp, usize, f32, i32, mem};
 use texture_allocator::{ArrayAllocationTracker, FreeRectSlice};
 #[cfg(feature = "pathfinder")]
@@ -57,16 +57,28 @@ pub struct RenderTargetContext<'a, 'rc> 
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub data_stores: &'a DataStores,
     pub surfaces: &'a [SurfaceInfo],
     pub scratch: &'a PrimitiveScratchBuffer,
     pub screen_world_rect: WorldRect,
     pub globals: &'a FrameGlobalResources,
 }
 
+impl<'a, 'rc> RenderTargetContext<'a, 'rc> {
+    /// Returns true if a picture has a surface that is visible.
+    pub fn is_picture_surface_visible(&self, index: PictureIndex) -> bool {
+        match self.prim_store.pictures[index.0].raster_config {
+            Some(ref raster_config) => {
+                self.surfaces[raster_config.surface_index.0].surface.is_some()
+            }
+            None => false,
+        }
+    }
+}
+
 /// Represents a number of rendering operations on a surface.
 ///
 /// In graphics parlance, a "render target" usually means "a surface (texture or
 /// framebuffer) bound to the output of a shader". This trait has a slightly
 /// different meaning, in that it represents the operations on that surface
 /// _before_ it's actually bound and rendered. So a `RenderTarget` is built by
 /// the `RenderBackend` by inserting tasks, and then shipped over to the
 /// `Renderer` where a device surface is resolved and the tasks are transformed
@@ -343,17 +355,16 @@ pub struct GlyphJob;
 /// See `RenderTarget`.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ColorRenderTarget {
     pub alpha_batch_containers: Vec<AlphaBatchContainer>,
     // List of blur operations to apply for this render target.
     pub vertical_blurs: Vec<BlurInstance>,
     pub horizontal_blurs: Vec<BlurInstance>,
-    pub readbacks: Vec<DeviceIntRect>,
     pub scalings: Vec<ScalingInstance>,
     pub blits: Vec<BlitJob>,
     // List of frame buffer outputs for this render target.
     pub outputs: Vec<FrameOutput>,
     alpha_tasks: Vec<RenderTaskId>,
     screen_size: DeviceIntSize,
     // Track the used rect of the render target, so that
     // we can set a scissor rect and only clear to the
@@ -365,17 +376,16 @@ impl RenderTarget for ColorRenderTarget 
     fn new(
         screen_size: DeviceIntSize,
         _: bool,
     ) -> Self {
         ColorRenderTarget {
             alpha_batch_containers: Vec::new(),
             vertical_blurs: Vec::new(),
             horizontal_blurs: Vec::new(),
-            readbacks: Vec::new(),
             scalings: Vec::new(),
             blits: Vec::new(),
             outputs: Vec::new(),
             alpha_tasks: Vec::new(),
             screen_size,
             used_rect: DeviceIntRect::zero(),
         }
     }
@@ -497,19 +507,16 @@ impl RenderTarget for ColorRenderTarget 
             RenderTaskKind::CacheMask(..) |
             RenderTaskKind::LineDecoration(..) => {
                 panic!("Should not be added to color target!");
             }
             RenderTaskKind::Glyph(..) => {
                 // FIXME(pcwalton): Support color glyphs.
                 panic!("Glyphs should not be added to color target!");
             }
-            RenderTaskKind::Readback(device_rect) => {
-                self.readbacks.push(device_rect);
-            }
             RenderTaskKind::Scaling(..) => {
                 self.scalings.push(ScalingInstance {
                     task_address: render_tasks.get_task_address(task_id),
                     src_task_address: render_tasks.get_task_address(task.children[0]),
                 });
             }
             RenderTaskKind::Blit(ref task_info) => {
                 match task_info.source {
@@ -632,17 +639,16 @@ impl RenderTarget for AlphaRenderTarget 
             }
             ClearMode::DontCare => {}
             ClearMode::Transparent => {
                 panic!("bug: invalid clear mode for alpha task");
             }
         }
 
         match task.kind {
-            RenderTaskKind::Readback(..) |
             RenderTaskKind::Picture(..) |
             RenderTaskKind::Blit(..) |
             RenderTaskKind::Border(..) |
             RenderTaskKind::LineDecoration(..) |
             RenderTaskKind::Glyph(..) => {
                 panic!("BUG: should not be added to alpha target!");
             }
             RenderTaskKind::VerticalBlur(ref info) => {
@@ -814,17 +820,16 @@ impl TextureCacheRenderTarget {
             }
             RenderTaskKind::Glyph(ref mut task_info) => {
                 self.add_glyph_task(task_info, target_rect.0)
             }
             RenderTaskKind::VerticalBlur(..) |
             RenderTaskKind::Picture(..) |
             RenderTaskKind::ClipRegion(..) |
             RenderTaskKind::CacheMask(..) |
-            RenderTaskKind::Readback(..) |
             RenderTaskKind::Scaling(..) => {
                 panic!("BUG: unexpected task kind for texture cache target");
             }
         }
     }
 
     #[cfg(feature = "pathfinder")]
     fn add_glyph_task(&mut self, task_info: &mut GlyphTask, target_rect: DeviceIntRect) {
--- a/gfx/wr/wrench/reftests/blend/multiply-2.yaml
+++ b/gfx/wr/wrench/reftests/blend/multiply-2.yaml
@@ -1,13 +1,14 @@
+# test that we handle the backdrop size to be smaller than the source
 ---
 root:
   items:
         - type: rect
-          bounds: [0, 0, 100, 100]
-          color: [0, 255, 0]
+          bounds: [25, 25, 50, 50]
+          color: green
         - type: stacking-context
           bounds: [0, 0, 100, 100]
           mix-blend-mode: multiply
           items:
             - type: rect
               bounds: [0, 0, 100, 100]
-              color: [255, 128, 0]
+              color: green
rename from gfx/wr/wrench/reftests/blend/multiply-2-ref.yaml
rename to gfx/wr/wrench/reftests/blend/multiply-3-ref.yaml
--- a/gfx/wr/wrench/reftests/blend/multiply-3.yaml
+++ b/gfx/wr/wrench/reftests/blend/multiply-3.yaml
@@ -3,15 +3,11 @@ root:
   items:
         - type: rect
           bounds: [0, 0, 100, 100]
           color: [0, 255, 0]
         - type: stacking-context
           bounds: [0, 0, 100, 100]
           mix-blend-mode: multiply
           items:
-            - type: stacking-context
+            - type: rect
               bounds: [0, 0, 100, 100]
-              mix-blend-mode: multiply
-              items:
-                - type: rect
-                  bounds: [0, 0, 100, 100]
-                  color: [255, 128, 0]
+              color: [255, 128, 0]
copy from gfx/wr/wrench/reftests/blend/multiply-3.yaml
copy to gfx/wr/wrench/reftests/blend/multiply-4.yaml
--- a/gfx/wr/wrench/reftests/blend/reftest.list
+++ b/gfx/wr/wrench/reftests/blend/reftest.list
@@ -1,11 +1,12 @@
 == multiply.yaml multiply-ref.yaml
-== multiply-2.yaml multiply-2-ref.yaml
-== color_targets(3) alpha_targets(0) multiply-3.yaml multiply-2-ref.yaml
+== multiply-2.yaml multiply-ref.yaml
+== multiply-3.yaml multiply-3-ref.yaml
+== color_targets(2) alpha_targets(0) multiply-4.yaml multiply-3-ref.yaml
 == difference.yaml difference-ref.yaml
 fuzzy(1,10000) == difference-transparent.yaml difference-transparent-ref.yaml
 == darken.yaml darken-ref.yaml
 == lighten.yaml lighten-ref.yaml
 
 == repeated-difference.yaml repeated-difference-ref.yaml
 
 == isolated.yaml isolated-ref.yaml
@@ -17,8 +18,9 @@ fuzzy(1,10000) == difference-transparent
 == large.yaml large-ref.yaml
 
 # fuzzy because dithering is different for gradients
 # drawn in different render targets
 fuzzy(1,2502) == transparent-composite-1.yaml transparent-composite-1-ref.yaml
 fuzzy(1,2502) == transparent-composite-2.yaml transparent-composite-2-ref.yaml
 
 == multi-mix-blend-mode.yaml multi-mix-blend-mode-ref.yaml
+fuzzy(50,5) == transform-source.yaml transform-source-ref.yaml
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transform-source-ref.yaml
@@ -0,0 +1,13 @@
+---
+root:
+  items:
+    - type: stacking-context
+      bounds: [0, 0, 0, 0]
+      transform: rotate-z(60)
+      items:
+        - type: rect
+          bounds: [25, -100, 150, 150]
+          color: blue
+    - type: rect
+      bounds: [25, 25, 100, 100]
+      color: black
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/blend/transform-source.yaml
@@ -0,0 +1,15 @@
+# test that we handle the source stacking context being transformed
+---
+root:
+  items:
+    - type: rect
+      bounds: [25, 25, 100, 100]
+      color: green
+    - type: stacking-context
+      bounds: [0, 0, 0, 0]
+      mix-blend-mode: multiply
+      transform: rotate-z(60)
+      items:
+        - type: rect
+          bounds: [25, -100, 150, 150]
+          color: blue
--- a/gfx/wr/wrench/src/main.rs
+++ b/gfx/wr/wrench/src/main.rs
@@ -528,31 +528,33 @@ fn main() {
 fn render<'a>(
     wrench: &mut Wrench,
     window: &mut WindowWrapper,
     size: DeviceIntSize,
     events_loop: &mut Option<winit::EventsLoop>,
     subargs: &clap::ArgMatches<'a>,
 ) {
     let input_path = subargs.value_of("INPUT").map(PathBuf::from).unwrap();
+    let mut show_stats = false;
 
     // If the input is a directory, we are looking at a capture.
-    let mut thing = if input_path.as_path().is_dir() {
+    let mut thing = if input_path.is_dir() {
         let mut documents = wrench.api.load_capture(input_path);
         println!("loaded {:?}", documents.iter().map(|cd| cd.document_id).collect::<Vec<_>>());
         let captured = documents.swap_remove(0);
         window.resize(captured.window_size);
         wrench.document_id = captured.document_id;
         Box::new(captured) as Box<WrenchThing>
     } else {
         let extension = input_path
             .extension()
             .expect("Tried to render with an unknown file type.")
             .to_str()
             .expect("Tried to render with an unknown file type.");
+        show_stats = true; // show when invoked on single files
 
         match extension {
             "yaml" => Box::new(YamlFrameReader::new_from_args(subargs)) as Box<WrenchThing>,
             "bin" => Box::new(BinaryFrameReader::new_from_args(subargs)) as Box<WrenchThing>,
             _ => panic!("Tried to render with an unknown file type."),
         }
     };
 
@@ -742,19 +744,24 @@ fn render<'a>(
             }
         }
 
         if do_render {
             if show_help {
                 wrench.show_onscreen_help();
             }
 
-            wrench.render();
+            let results = wrench.render();
             window.swap_buffers();
 
+            if show_stats {
+                show_stats = false;
+                println!("{:#?}", results.stats);
+            }
+
             if do_loop {
                 thing.next_frame();
             }
         }
 
         winit::ControlFlow::Continue
     };
 
--- a/layout/reftests/css-blending/reftest.list
+++ b/layout/reftests/css-blending/reftest.list
@@ -1,16 +1,16 @@
 == blend-canvas.html blend-canvas-ref.html
 == blend-constant-background-color.html blend-constant-background-color-ref.html
-fuzzy-if(webrender,1-3,1313-7888) == blend-gradient-background-color.html blend-gradient-background-color-ref.html
+fuzzy-if(webrender,1-3,1291-7888) == blend-gradient-background-color.html blend-gradient-background-color-ref.html
 == blend-image.html blend-image-ref.html
 == blend-difference-stacking.html blend-difference-stacking-ref.html
 
 fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu),0-1,0-10000) fuzzy-if(skiaContent,0-1,0-30000) == background-blending-alpha.html background-blending-alpha-ref.html
-fuzzy-if(webrender,1-3,1313-7888) == background-blending-gradient-color.html background-blending-gradient-color-ref.html
+fuzzy-if(webrender,1-3,1291-7888) == background-blending-gradient-color.html background-blending-gradient-color-ref.html
 fuzzy-if(azureSkiaGL,0-3,0-7597) fuzzy-if(cocoaWidget,0-3,0-7597) fuzzy-if(d2d,0-1,0-3800) fuzzy-if(d3d11,0-1,0-4200) fuzzy-if(skiaContent,0-2,0-9450) fuzzy-if(webrender,1-5,3938-23925) == background-blending-gradient-gradient.html background-blending-gradient-gradient-ref.html
 fuzzy-if(azureSkiaGL,0-2,0-7174) fuzzy-if(webrender,1-3,1288-7888) == background-blending-gradient-image.html background-blending-gradient-color-ref.html
 fuzzy-if(azureSkia||d2d||gtkWidget,0-1,0-10000) == background-blending-image-color-jpg.html background-blending-image-color-ref.html
 == background-blending-image-color-png.html background-blending-image-color-ref.html
 == background-blending-image-color-svg.html background-blending-image-color-ref.html
 fuzzy-if(azureSkiaGL,0-2,0-7174) fuzzy-if(webrender,1-3,1288-7888) == background-blending-image-gradient.html background-blending-gradient-color-ref.html
 == background-blending-image-image.html background-blending-image-color-ref.html
 == background-blending-isolation.html background-blending-isolation-ref.html