Bug 1621390 - Optimize backdrop primitives by collapsing to clear color. r=lsalzman,Bert
authorGlenn Watson <gw@intuitionlibrary.com>
Mon, 16 Mar 2020 03:47:53 +0000
changeset 518887 6199f7b91e8bde7b7965585842d7a72e2e406e3e
parent 518886 0fdeafcdcb41a6e7def0650ef1d93e3e5bef9299
child 518888 2786da95404d56478d501b0e15aefaa68ce9e02d
push id37218
push userrmaries@mozilla.com
push dateMon, 16 Mar 2020 09:28:04 +0000
treeherdermozilla-central@6199f7b91e8b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslsalzman, Bert
bugs1621390
milestone76.0a1
first release with
nightly linux32
6199f7b91e8b / 76.0a1 / 20200316092804 / files
nightly linux64
6199f7b91e8b / 76.0a1 / 20200316092804 / files
nightly mac
6199f7b91e8b / 76.0a1 / 20200316092804 / files
nightly win32
6199f7b91e8b / 76.0a1 / 20200316092804 / files
nightly win64
6199f7b91e8b / 76.0a1 / 20200316092804 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1621390 - Optimize backdrop primitives by collapsing to clear color. r=lsalzman,Bert Differential Revision: https://phabricator.services.mozilla.com/D66362
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
layout/reftests/transform-3d/reftest.list
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -14,17 +14,18 @@ use crate::gpu_types::{BrushFlags, Brush
 use crate::gpu_types::{ClipMaskInstance, SplitCompositeInstance, BrushShaderKind};
 use crate::gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use crate::gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
 use crate::gpu_types::{ImageBrushData, get_shader_opacity};
 use crate::internal_types::{FastHashMap, SavedTargetIndex, Swizzle, TextureSource, Filter};
 use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
 use crate::prim_store::{DeferredResolve, EdgeAaSegmentMask, PrimitiveInstanceKind, PrimitiveVisibilityIndex, PrimitiveVisibilityMask};
 use crate::prim_store::{VisibleGradientTile, PrimitiveInstance, PrimitiveOpacity, SegmentInstanceIndex};
-use crate::prim_store::{BrushSegment, ClipMaskKind, ClipTaskIndex, VECS_PER_SEGMENT, SpaceMapper};
+use crate::prim_store::{BrushSegment, ClipMaskKind, ClipTaskIndex, PrimitiveVisibilityFlags};
+use crate::prim_store::{VECS_PER_SEGMENT, SpaceMapper};
 use crate::prim_store::image::ImageSource;
 use crate::render_target::RenderTargetContext;
 use crate::render_task_graph::{RenderTaskId, RenderTaskGraph};
 use crate::render_task::RenderTaskAddress;
 use crate::renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use crate::renderer::{BLOCKS_PER_UV_RECT, MAX_VERTEX_TEXTURE_WIDTH};
 use crate::resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache};
 use smallvec::SmallVec;
@@ -188,16 +189,26 @@ impl AlphaBatchList {
             item_rects: Vec::new(),
             current_z_id: ZBufferId::invalid(),
             current_batch_index: usize::MAX,
             break_advanced_blend_batches,
             lookback_count,
         }
     }
 
+    /// Clear all current batches in this list. This is typically used
+    /// when a primitive is encountered that occludes all previous
+    /// content in this batch list.
+    fn clear(&mut self) {
+        self.current_batch_index = usize::MAX;
+        self.current_z_id = ZBufferId::invalid();
+        self.batches.clear();
+        self.item_rects.clear();
+    }
+
     pub fn set_params_and_get_batch(
         &mut self,
         key: BatchKey,
         features: BatchFeatures,
         // The bounding box of everything at this Z plane. We expect potentially
         // multiple primitive segments coming with the same `z_id`.
         z_bounding_rect: &PictureRect,
         z_id: ZBufferId,
@@ -285,16 +296,24 @@ impl OpaqueBatchList {
         OpaqueBatchList {
             batches: Vec::new(),
             pixel_area_threshold_for_new_batch,
             current_batch_index: usize::MAX,
             lookback_count,
         }
     }
 
+    /// Clear all current batches in this list. This is typically used
+    /// when a primitive is encountered that occludes all previous
+    /// content in this batch list.
+    fn clear(&mut self) {
+        self.current_batch_index = usize::MAX;
+        self.batches.clear();
+    }
+
     pub fn set_params_and_get_batch(
         &mut self,
         key: BatchKey,
         features: BatchFeatures,
         // The bounding box of everything at the current Z, whatever it is. We expect potentially
         // multiple primitive segments produced by a primitive, which we allow to check
         // `current_batch_index` instead of iterating the batches.
         z_bounding_rect: &PictureRect,
@@ -497,16 +516,24 @@ impl AlphaBatchBuilder {
             alpha_batch_list: AlphaBatchList::new(break_advanced_blend_batches, lookback_count),
             opaque_batch_list: OpaqueBatchList::new(batch_area_threshold, lookback_count),
             render_task_id,
             render_task_address,
             vis_mask,
         }
     }
 
+    /// Clear all current batches in this builder. This is typically used
+    /// when a primitive is encountered that occludes all previous
+    /// content in this batch list.
+    fn clear(&mut self) {
+        self.alpha_batch_list.clear();
+        self.opaque_batch_list.clear();
+    }
+
     pub fn build(
         mut self,
         batch_containers: &mut Vec<AlphaBatchContainer>,
         merged_batches: &mut AlphaBatchContainer,
         task_rect: DeviceIntRect,
         task_scissor_rect: Option<DeviceIntRect>,
     ) {
         self.opaque_batch_list.finalize();
@@ -648,16 +675,24 @@ impl BatchBuilder {
                         polygons_address,
                         z: z_id,
                     }),
                 );
             }
         }
     }
 
+    /// Clear all current batchers. This is typically used when a primitive
+    /// is encountered that occludes all previous content in this batch list.
+    fn clear_batches(&mut self) {
+        for batcher in &mut self.batchers {
+            batcher.clear();
+        }
+    }
+
     /// Add a picture to a given batch builder.
     pub fn add_pic_to_batch(
         &mut self,
         pic: &PicturePrimitive,
         ctx: &RenderTargetContext,
         gpu_cache: &mut GpuCache,
         render_tasks: &RenderTaskGraph,
         deferred_resolves: &mut Vec<DeferredResolve>,
@@ -726,16 +761,26 @@ impl BatchBuilder {
 
         // TODO(gw): Calculating this for every primitive is a bit
         //           wasteful. We should probably cache this in
         //           the scroll node...
         let transform_kind = transform_id.transform_kind();
         let prim_info = &ctx.scratch.prim_info[prim_instance.visibility_info.0 as usize];
         let bounding_rect = &prim_info.clip_chain.pic_clip_rect;
 
+        // If this primitive is a backdrop, that means that it is known to cover
+        // the entire picture cache background. In that case, the renderer will
+        // use the backdrop color as a clear color, and so we can drop this
+        // primitive and any prior primitives from the batch lists for this
+        // picture cache slice.
+        if prim_info.flags.contains(PrimitiveVisibilityFlags::IS_BACKDROP) {
+            self.clear_batches();
+            return;
+        }
+
         let z_id = z_generator.next();
 
         let prim_common_data = &ctx.data_stores.as_common_data(&prim_instance);
         let prim_rect = LayoutRect::new(
             prim_instance.prim_origin,
             prim_common_data.prim_size
         );
 
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -10,17 +10,17 @@ use crate::spatial_tree::{SpatialTree, R
 use crate::composite::{CompositorKind, CompositeState};
 use crate::debug_render::DebugItem;
 use crate::gpu_cache::{GpuCache, GpuCacheHandle};
 use crate::gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
 use crate::gpu_types::TransformData;
 use crate::internal_types::{FastHashMap, PlaneSplitter, SavedTargetIndex};
 use crate::picture::{PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex, RecordedDirtyRegion};
 use crate::picture::{RetainedTiles, TileCacheInstance, DirtyRegion, SurfaceRenderTasks, SubpixelMode};
-use crate::picture::{TileCacheLogger};
+use crate::picture::{BackdropKind, TileCacheLogger};
 use crate::prim_store::{SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
 use crate::prim_store::{DeferredResolve, PrimitiveVisibilityMask};
 use crate::profiler::{FrameProfileCounters, TextureCacheProfileCounters, ResourceProfileCounters};
 use crate::render_backend::{DataStores, FrameStamp, FrameId};
 use crate::render_target::{RenderTarget, PictureCacheTarget, TextureCacheRenderTarget};
 use crate::render_target::{RenderTargetContext, RenderTargetKind};
 use crate::render_task_graph::{RenderTaskId, RenderTaskGraph, RenderTaskGraphCounters};
 use crate::render_task_graph::{RenderPassKind, RenderPass};
@@ -878,24 +878,29 @@ pub fn build_render_pass(
                 // first layer even if it's technically transparent.
                 // Otherwise, clear to transparent and composite with alpha.
                 // TODO(gw): We can detect per-tile opacity for the clear color here
                 //           which might be a significant win on some pages?
                 let forced_opaque = match tile_cache.background_color {
                     Some(color) => color.a >= 1.0,
                     None => false,
                 };
-                // TODO(gw): Once we have multiple slices enabled, take advantage of
-                //           option to skip clears if the slice is opaque.
-                let clear_color = if forced_opaque {
+                let mut clear_color = if forced_opaque {
                     Some(ColorF::WHITE)
                 } else {
                     Some(ColorF::TRANSPARENT)
                 };
 
+                // If this picture cache has a valid color backdrop, we will use
+                // that as the clear color, skipping the draw of the backdrop
+                // primitive (and anything prior to it) during batching.
+                if let Some(BackdropKind::Color { color }) = tile_cache.backdrop.kind {
+                    clear_color = Some(color);
+                }
+
                 // Create an alpha batcher for each of the tasks of this picture.
                 let mut batchers = Vec::new();
                 for task_id in &task_ids {
                     let task_id = *task_id;
                     let vis_mask = match render_tasks[task_id].kind {
                         RenderTaskKind::Picture(ref info) => info.vis_mask,
                         _ => unreachable!(),
                     };
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -115,17 +115,17 @@ use crate::internal_types::{FastHashMap,
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::{UvRectKind, ZBufferId};
 use plane_split::{Clipper, Polygon, Splitter};
 use crate::prim_store::{SpaceMapper, PrimitiveVisibilityMask, PointKey, PrimitiveTemplateKind};
 use crate::prim_store::{SpaceSnapper, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, RectangleKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
-use crate::prim_store::{ColorBindingStorage, ColorBindingIndex};
+use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveVisibilityFlags};
 use crate::print_tree::{PrintTree, PrintTreePrinter};
 use crate::render_backend::DataStores;
 use crate::render_task_graph::RenderTaskId;
 use crate::render_target::RenderTargetKind;
 use crate::render_task::{RenderTask, RenderTaskLocation, BlurTaskCache, ClearMode};
 use crate::resource_cache::{ResourceCache, ImageGeneration};
 use crate::scene::SceneProperties;
 use crate::spatial_tree::CoordinateSystemId;
@@ -1247,17 +1247,17 @@ impl Tile {
             .unwrap_or_else(DeviceRect::zero);
 
         // Check if this tile can be considered opaque. Opacity state must be updated only
         // after all early out checks have been performed. Otherwise, we might miss updating
         // the native surface next time this tile becomes visible.
         let clipped_rect = self.current_descriptor.local_valid_rect
             .intersection(&ctx.local_clip_rect)
             .unwrap_or_else(PictureRect::zero);
-        let mut is_opaque = ctx.backdrop.rect.contains_rect(&clipped_rect);
+        let mut is_opaque = ctx.backdrop.opaque_rect.contains_rect(&clipped_rect);
 
         if self.has_compositor_surface {
             // If we found primitive(s) that are ordered _after_ the first compositor
             // surface, _and_ intersect with any compositor surface, then we will need
             // to draw this tile with alpha blending, as an overlay to the compositor surface.
             let fg_world_valid_rect = ctx.pic_to_world_mapper
                 .map(&self.fg_local_valid_rect)
                 .expect("bug: map fg local valid rect");
@@ -1333,36 +1333,36 @@ impl Tile {
         }
 
         // See if this tile is a simple color, in which case we can just draw
         // it as a rect, and avoid allocating a texture surface and drawing it.
         // TODO(gw): Initial native compositor interface doesn't support simple
         //           color tiles. We can definitely support this in DC, so this
         //           should be added as a follow up.
         let is_simple_prim =
-            ctx.backdrop.kind.can_be_promoted_to_compositor_surface() &&
+            ctx.backdrop.kind.is_some() &&
             self.current_descriptor.prims.len() == 1 &&
             self.is_opaque &&
             supports_simple_prims;
 
         // Set up the backing surface for this tile.
         let surface = if is_simple_prim {
             // If we determine the tile can be represented by a color, set the
             // surface unconditionally (this will drop any previously used
             // texture cache backing surface).
             match ctx.backdrop.kind {
-                BackdropKind::Color { color } => {
+                Some(BackdropKind::Color { color }) => {
                     TileSurface::Color {
                         color,
                     }
                 }
-                BackdropKind::Clear => {
+                Some(BackdropKind::Clear) => {
                     TileSurface::Clear
                 }
-                BackdropKind::Image => {
+                None => {
                     // This should be prevented by the is_simple_prim check above.
                     unreachable!();
                 }
             }
         } else {
             // If this tile will be backed by a surface, we want to retain
             // the texture handle from the previous frame, if possible. If
             // the tile was previously a color, or not set, then just set
@@ -1802,52 +1802,39 @@ impl ::std::fmt::Display for RecordedDir
 
 impl ::std::fmt::Debug for RecordedDirtyRegion {
     fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
         ::std::fmt::Display::fmt(self, f)
     }
 }
 
 #[derive(Debug, Copy, Clone)]
-enum BackdropKind {
+pub enum BackdropKind {
     Color {
         color: ColorF,
     },
     Clear,
-    Image,
-}
-
-impl BackdropKind {
-    /// Returns true if the compositor can directly draw this backdrop.
-    fn can_be_promoted_to_compositor_surface(&self) -> bool {
-        match self {
-            BackdropKind::Color { .. } | BackdropKind::Clear => true,
-            BackdropKind::Image => false,
-        }
-    }
 }
 
 /// Stores information about the calculated opaque backdrop of this slice.
 #[derive(Debug, Copy, Clone)]
-struct BackdropInfo {
+pub struct BackdropInfo {
     /// The picture space rectangle that is known to be opaque. This is used
     /// to determine where subpixel AA can be used, and where alpha blending
     /// can be disabled.
-    rect: PictureRect,
+    pub opaque_rect: PictureRect,
     /// Kind of the backdrop
-    kind: BackdropKind,
+    pub kind: Option<BackdropKind>,
 }
 
 impl BackdropInfo {
     fn empty() -> Self {
         BackdropInfo {
-            rect: PictureRect::zero(),
-            kind: BackdropKind::Color {
-                color: ColorF::BLACK,
-            },
+            opaque_rect: PictureRect::zero(),
+            kind: None,
         }
     }
 }
 
 #[derive(Clone)]
 pub struct TileCacheLoggerSlice {
     pub serialized_slice: String,
     pub local_to_world_transform: Transform3D<f32, PicturePixel, WorldPixel>,
@@ -2210,17 +2197,17 @@ pub struct TileCacheInstance {
     /// The local clip rect, from the shared clips of this picture.
     local_clip_rect: PictureRect,
     /// The surface index that this tile cache will be drawn into.
     surface_index: SurfaceIndex,
     /// The background color from the renderer. If this is set opaque, we know it's
     /// fine to clear the tiles to this and allow subpixel text on the first slice.
     pub background_color: Option<ColorF>,
     /// Information about the calculated backdrop content of this cache.
-    backdrop: BackdropInfo,
+    pub backdrop: BackdropInfo,
     /// The allowed subpixel mode for this surface, which depends on the detected
     /// opacity of the background.
     pub subpixel_mode: SubpixelMode,
     /// A list of clip handles that exist on every (top-level) primitive in this picture.
     /// It's often the case that these are root / fixed position clips. By handling them
     /// here, we can avoid applying them to the items, which reduces work, but more importantly
     /// reduces invalidations.
     pub shared_clips: Vec<ClipDataHandle>,
@@ -2825,41 +2812,41 @@ impl TileCacheInstance {
         clip_store: &ClipStore,
         pictures: &[PicturePrimitive],
         resource_cache: &mut ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
         color_bindings: &ColorBindingStorage,
         image_instances: &ImageInstanceStorage,
         surface_stack: &[SurfaceIndex],
         composite_state: &mut CompositeState,
-    ) -> bool {
+    ) -> Option<PrimitiveVisibilityFlags> {
         // This primitive exists on the last element on the current surface stack.
         let prim_surface_index = *surface_stack.last().unwrap();
 
         // If the primitive is completely clipped out by the clip chain, there
         // is no need to add it to any primitive dependencies.
         let prim_clip_chain = match prim_clip_chain {
             Some(prim_clip_chain) => prim_clip_chain,
-            None => return false,
+            None => return None,
         };
 
         self.map_local_to_surface.set_target_spatial_node(
             prim_spatial_node_index,
             frame_context.spatial_tree,
         );
 
         // Map the primitive local rect into picture space.
         let prim_rect = match self.map_local_to_surface.map(&local_prim_rect) {
             Some(rect) => rect,
-            None => return false,
+            None => return None,
         };
 
         // If the rect is invalid, no need to create dependencies.
         if prim_rect.size.is_empty_or_negative() {
-            return false;
+            return None;
         }
 
         // If the primitive is directly drawn onto this picture cache surface, then
         // the pic_clip_rect is in the same space. If not, we need to map it from
         // the surface space into the picture cache space.
         let on_picture_surface = prim_surface_index == self.surface_index;
         let pic_clip_rect = if on_picture_surface {
             prim_clip_chain.pic_clip_rect
@@ -2889,33 +2876,33 @@ impl TileCacheInstance {
                 // Map the rect into the parent surface, and inflate if this surface requires
                 // it. If the rect can't be mapping (e.g. due to an invalid transform) then
                 // just bail out from the dependencies and cull this primitive.
                 current_pic_clip_rect = match map_local_to_surface.map(&current_pic_clip_rect) {
                     Some(rect) => {
                         rect.inflate(surface.inflation_factor, surface.inflation_factor)
                     }
                     None => {
-                        return false;
+                        return None;
                     }
                 };
 
                 current_spatial_node_index = surface.surface_spatial_node_index;
             }
 
             current_pic_clip_rect
         };
 
         // Get the tile coordinates in the picture space.
         let (p0, p1) = self.get_tile_coords_for_rect(&pic_clip_rect);
 
         // If the primitive is outside the tiling rects, it's known to not
         // be visible.
         if p0.x == p1.x || p0.y == p1.y {
-            return false;
+            return None;
         }
 
         // Build the list of resources that this primitive has dependencies on.
         let mut prim_info = PrimitiveDependencyInfo::new(
             prim_instance.uid(),
             prim_rect.origin,
             pic_clip_rect,
         );
@@ -2970,17 +2957,20 @@ impl TileCacheInstance {
                     //           case for background rects is that they don't have animated opacity.
                     let color = match data_stores.prim[data_handle].kind {
                         PrimitiveTemplateKind::Rectangle { color, .. } => {
                             frame_context.scene_properties.resolve_color(&color)
                         }
                         _ => unreachable!(),
                     };
                     if color.a >= 1.0 {
-                        backdrop_candidate = Some(BackdropKind::Color { color });
+                        backdrop_candidate = Some(BackdropInfo {
+                            opaque_rect: pic_clip_rect,
+                            kind: Some(BackdropKind::Color { color }),
+                        });
                     }
                 } else {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         prim_info.opacity_bindings.push(OpacityBinding::from(*binding));
                     }
                 }
 
@@ -2992,19 +2982,27 @@ impl TileCacheInstance {
             }
             PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
                 let image_data = &data_stores.image[data_handle].kind;
                 let image_instance = &image_instances[image_instance_index];
                 let opacity_binding_index = image_instance.opacity_binding_index;
 
                 if opacity_binding_index == OpacityBindingIndex::INVALID {
                     if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) {
-                        // If this image is opaque, it can be considered as a possible opaque backdrop
-                        if image_properties.descriptor.is_opaque() {
-                            backdrop_candidate = Some(BackdropKind::Image);
+                        // For an image to be a possible opaque backdrop, it must:
+                        // - Have a valid, opaque image descriptor
+                        // - Not use tiling (since they can fail to draw)
+                        // - Not having any spacing / padding
+                        if image_properties.descriptor.is_opaque() &&
+                           image_properties.tiling.is_none() &&
+                           image_data.tile_spacing == LayoutSize::zero() {
+                            backdrop_candidate = Some(BackdropInfo {
+                                opaque_rect: pic_clip_rect,
+                                kind: None,
+                            });
                         }
                     }
                 } else {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         prim_info.opacity_bindings.push(OpacityBinding::from(*binding));
                     }
                 }
@@ -3204,17 +3202,17 @@ impl TileCacheInstance {
                 prim_info.images.push(ImageDependency {
                     key: border_data.request.key,
                     generation: resource_cache.get_image_generation(border_data.request.key),
                 });
             }
             PrimitiveInstanceKind::PushClipChain |
             PrimitiveInstanceKind::PopClipChain => {
                 // Early exit to ensure this doesn't get added as a dependency on the tile.
-                return false;
+                return None;
             }
             PrimitiveInstanceKind::TextRun { data_handle, .. } => {
                 // Only do these checks if we haven't already disabled subpx
                 // text rendering for this slice.
                 if self.subpixel_mode == SubpixelMode::Allow && !self.is_opaque() {
                     let run_data = &data_stores.text_run[data_handle];
 
                     // Only care about text runs that have requested subpixel rendering.
@@ -3224,46 +3222,51 @@ impl TileCacheInstance {
                         FontRenderMode::Subpixel => true,
                         FontRenderMode::Alpha | FontRenderMode::Mono => false,
                     };
 
                     // If a text run is on a child surface, the subpx mode will be
                     // correctly determined as we recurse through pictures in take_context.
                     if on_picture_surface
                         && subpx_requested
-                        && !self.backdrop.rect.contains_rect(&pic_clip_rect) {
+                        && !self.backdrop.opaque_rect.contains_rect(&pic_clip_rect) {
                         self.subpixel_mode = SubpixelMode::Deny;
                     }
                 }
             }
             PrimitiveInstanceKind::Clear { .. } => {
-                backdrop_candidate = Some(BackdropKind::Clear);
+                backdrop_candidate = Some(BackdropInfo {
+                    opaque_rect: pic_clip_rect,
+                    kind: Some(BackdropKind::Clear),
+                });
             }
             PrimitiveInstanceKind::LineDecoration { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
             PrimitiveInstanceKind::ConicGradient { .. } |
             PrimitiveInstanceKind::Backdrop { .. } => {
                 // These don't contribute dependencies
             }
         };
 
         // If this primitive considers itself a backdrop candidate, apply further
         // checks to see if it matches all conditions to be a backdrop.
+        let mut vis_flags = PrimitiveVisibilityFlags::empty();
+
         if let Some(backdrop_candidate) = backdrop_candidate {
-            let is_suitable_backdrop = match backdrop_candidate {
-                BackdropKind::Clear => {
+            let is_suitable_backdrop = match backdrop_candidate.kind {
+                Some(BackdropKind::Clear) => {
                     // Clear prims are special - they always end up in their own slice,
                     // and always set the backdrop. In future, we hope to completely
                     // remove clear prims, since they don't integrate with the compositing
                     // system cleanly.
                     true
                 }
-                BackdropKind::Image | BackdropKind::Color { .. } => {
+                Some(BackdropKind::Color { .. }) | None => {
                     // Check a number of conditions to see if we can consider this
                     // primitive as an opaque backdrop rect. Several of these are conservative
                     // checks and could be relaxed in future. However, these checks
                     // are quick and capture the common cases of background rects and images.
                     // Specifically, we currently require:
                     //  - The primitive is on the main picture cache surface.
                     //  - Same coord system as picture cache (ensures rects are axis-aligned).
                     //  - No clip masks exist.
@@ -3276,22 +3279,35 @@ impl TileCacheInstance {
                         prim_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id
                     };
 
                     same_coord_system && on_picture_surface
                 }
             };
 
             if is_suitable_backdrop
-                && !prim_clip_chain.needs_mask
-                && pic_clip_rect.contains_rect(&self.backdrop.rect) {
-                self.backdrop = BackdropInfo {
-                    rect: pic_clip_rect,
-                    kind: backdrop_candidate,
-                };
+                && self.external_surfaces.is_empty()
+                && !prim_clip_chain.needs_mask {
+
+                if backdrop_candidate.opaque_rect.contains_rect(&self.backdrop.opaque_rect) {
+                    self.backdrop.opaque_rect = backdrop_candidate.opaque_rect;
+                }
+
+                if let Some(kind) = backdrop_candidate.kind {
+                    if backdrop_candidate.opaque_rect.contains_rect(&self.local_rect) {
+                        // If we have a color backdrop, mark the visibility flags
+                        // of the primitive so it is skipped during batching (and
+                        // also clears any previous primitives).
+                        if let BackdropKind::Color { .. } = kind {
+                            vis_flags |= PrimitiveVisibilityFlags::IS_BACKDROP;
+                        }
+
+                        self.backdrop.kind = Some(kind);
+                    }
+                }
             }
         }
 
         // Record any new spatial nodes in the used list.
         self.used_spatial_nodes.extend(&prim_info.spatial_nodes);
 
         // Truncate the lengths of dependency arrays to the max size we can handle.
         // Any arrays this size or longer will invalidate every frame.
@@ -3307,17 +3323,17 @@ impl TileCacheInstance {
                 // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile?
                 let key = TileOffset::new(x, y);
                 let tile = self.tiles.get_mut(&key).expect("bug: no tile");
 
                 tile.add_prim_dependency(&prim_info);
             }
         }
 
-        true
+        Some(vis_flags)
     }
 
     /// Print debug information about this picture cache to a tree printer.
     fn print(&self) {
         // TODO(gw): This initial implementation is very basic - just printing
         //           the picture cache state to stdout. In future, we can
         //           make this dump each frame to a file, and produce a report
         //           stating which frames had invalidations. This will allow
@@ -3375,18 +3391,18 @@ impl TileCacheInstance {
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
             frame_context.global_screen_world_rect,
             frame_context.spatial_tree,
         );
 
         // Register the opaque region of this tile cache as an occluder, which
         // is used later in the frame to occlude other tiles.
-        if self.backdrop.rect.is_well_formed_and_nonempty() {
-            let backdrop_rect = self.backdrop.rect
+        if self.backdrop.opaque_rect.is_well_formed_and_nonempty() {
+            let backdrop_rect = self.backdrop.opaque_rect
                 .intersection(&self.local_rect)
                 .and_then(|r| {
                     r.intersection(&self.local_clip_rect)
                 });
 
             if let Some(backdrop_rect) = backdrop_rect {
                 let world_backdrop_rect = map_pic_to_world
                     .map(&backdrop_rect)
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1514,16 +1514,30 @@ impl PrimitiveVisibilityMask {
     pub fn is_empty(&self) -> bool {
         self.bits == 0
     }
 
     /// The maximum number of supported dirty regions.
     pub const MAX_DIRTY_REGIONS: usize = 8 * mem::size_of::<PrimitiveVisibilityMask>();
 }
 
+bitflags! {
+    /// A set of bitflags that can be set in the visibility information
+    /// for a primitive instance. This can be used to control how primitives
+    /// are treated during batching.
+    // TODO(gw): We should also move `is_compositor_surface` to be part of
+    //           this flags struct.
+    #[cfg_attr(feature = "capture", derive(Serialize))]
+    pub struct PrimitiveVisibilityFlags: u16 {
+        /// Implies that this primitive covers the entire picture cache slice,
+        /// and can thus be dropped during batching and drawn with clear color.
+        const IS_BACKDROP = 1;
+    }
+}
+
 /// Information stored for a visible primitive about the visible
 /// rect and associated clip information.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct PrimitiveVisibility {
     /// The clip chain instance that was built for this primitive.
     pub clip_chain: ClipChainInstance,
 
     /// The current world rect, clipped to screen / dirty rect boundaries.
@@ -1534,16 +1548,20 @@ pub struct PrimitiveVisibility {
 
     /// An index into the clip task instances array in the primitive
     /// store. If this is ClipTaskIndex::INVALID, then the primitive
     /// has no clip mask. Otherwise, it may store the offset of the
     /// global clip mask task for this primitive, or the first of
     /// a list of clip task ids (one per segment).
     pub clip_task_index: ClipTaskIndex,
 
+    /// A set of flags that define how this primitive should be handled
+    /// during batching of visibile primitives.
+    pub flags: PrimitiveVisibilityFlags,
+
     /// A mask defining which of the dirty regions this primitive is visible in.
     pub visibility_mask: PrimitiveVisibilityMask,
 
     /// The current combined local clip for this primitive, from
     /// the primitive local clip above and the current clip chain.
     pub combined_local_clip_rect: LayoutRect,
 }
 
@@ -2090,16 +2108,17 @@ impl PrimitiveStore {
 
                     frame_state.scratch.prim_info.push(
                         PrimitiveVisibility {
                             clipped_world_rect: WorldRect::max_rect(),
                             clip_chain: ClipChainInstance::empty(),
                             clip_task_index: ClipTaskIndex::INVALID,
                             combined_local_clip_rect: LayoutRect::zero(),
                             visibility_mask: PrimitiveVisibilityMask::empty(),
+                            flags: PrimitiveVisibilityFlags::empty(),
                         }
                     );
 
                     prim_instance.visibility_info = vis_index;
                 } else {
                     if prim_local_rect.size.width <= 0.0 || prim_local_rect.size.height <= 0.0 {
                         if prim_instance.is_chased() {
                             println!("\tculled for zero local rectangle");
@@ -2151,41 +2170,51 @@ impl PrimitiveStore {
                             frame_state.resource_cache,
                             surface.device_pixel_scale,
                             &world_culling_rect,
                             &mut frame_state.data_stores.clip,
                             true,
                             prim_instance.is_chased(),
                         );
 
+                    // Primitive visibility flags default to empty, but may be supplied
+                    // by the `update_prim_dependencies` method below when picture caching
+                    // is active.
+                    let mut vis_flags = PrimitiveVisibilityFlags::empty();
+
                     if let Some(ref mut tile_cache) = frame_state.tile_cache {
                         // TODO(gw): Refactor how tile_cache is stored in frame_state
                         //           so that we can pass frame_state directly to
                         //           update_prim_dependencies, rather than splitting borrows.
-                        if !tile_cache.update_prim_dependencies(
+                        match tile_cache.update_prim_dependencies(
                             prim_instance,
                             cluster.spatial_node_index,
                             clip_chain.as_ref(),
                             prim_local_rect,
                             frame_context,
                             frame_state.data_stores,
                             frame_state.clip_store,
                             &self.pictures,
                             frame_state.resource_cache,
                             &self.opacity_bindings,
                             &self.color_bindings,
                             &self.images,
                             &frame_state.surface_stack,
                             &mut frame_state.composite_state,
                         ) {
-                            prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
-                            // Ensure the primitive clip is popped - perhaps we can use
-                            // some kind of scope to do this automatically in future.
-                            frame_state.clip_chain_stack.pop_clip();
-                            continue;
+                            Some(flags) => {
+                                vis_flags = flags;
+                            }
+                            None => {
+                                prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
+                                // Ensure the primitive clip is popped - perhaps we can use
+                                // some kind of scope to do this automatically in future.
+                                frame_state.clip_chain_stack.pop_clip();
+                                continue;
+                            }
                         }
                     }
 
                     // Ensure the primitive clip is popped
                     frame_state.clip_chain_stack.pop_clip();
 
                     let clip_chain = match clip_chain {
                         Some(clip_chain) => clip_chain,
@@ -2303,16 +2332,17 @@ impl PrimitiveStore {
 
                     frame_state.scratch.prim_info.push(
                         PrimitiveVisibility {
                             clipped_world_rect,
                             clip_chain,
                             clip_task_index: ClipTaskIndex::INVALID,
                             combined_local_clip_rect,
                             visibility_mask: PrimitiveVisibilityMask::empty(),
+                            flags: vis_flags,
                         }
                     );
 
                     prim_instance.visibility_info = vis_index;
 
                     self.request_resources_for_prim(
                         prim_instance,
                         cluster.spatial_node_index,
--- a/layout/reftests/transform-3d/reftest.list
+++ b/layout/reftests/transform-3d/reftest.list
@@ -89,10 +89,10 @@ fuzzy-if(webrender,0-16,0-132) == mask-l
 fuzzy(0-255,0-150) == split-intersect2.html split-intersect2-ref.html
 fuzzy(0-255,0-100) == split-non-ortho1.html split-non-ortho1-ref.html
 fuzzy-if(winWidget,0-150,0-120) == component-alpha-1.html component-alpha-1-ref.html
 == nested-transform-1.html nested-transform-1-ref.html
 == transform-geometry-1.html transform-geometry-1-ref.html
 == intermediate-1.html intermediate-1-ref.html
 == preserves3d-nested-filter-1.html preserves3d-nested-filter-1-ref.html
 != preserve3d-scale.html about:blank
-fuzzy-if(webrender,0-1,0-3) == perspective-overflow-1.html perspective-overflow-1-ref.html
+fuzzy-if(webrender,0-1,0-5) == perspective-overflow-1.html perspective-overflow-1-ref.html
 == 1544995-1.html 1544995-1-ref.html