Bug 1558106 - Support picture caching for multiple scroll roots. r=kvark
authorGlenn Watson <github@intuitionlibrary.com>
Thu, 13 Jun 2019 04:43:56 +0000
changeset 478637 431c5142ff3ad102328d90cb29470b20ccd1271e
parent 478636 855d557b82a20e1ed87af46bceac3b918534d37e
child 478638 a859291efa4e79246fcdb60d5b20b088fe993d7e
push id87800
push usergwatson@mozilla.com
push dateThu, 13 Jun 2019 07:26:39 +0000
treeherderautoland@431c5142ff3a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskvark
bugs1558106
milestone69.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 1558106 - Support picture caching for multiple scroll roots. r=kvark This patch implements the majority of the planned picture caching improvements. It supports most of the functionality required to (as a follow up) support OS compositor integration. It also improves on the robustness and functionality of the previous picture caching implementation. There are some expected temporary performance regressions in some cases (such as content that is constantly invalidating) and during initial page render when many render targets must be drawn to. These performance regressions will be resolved in follow up commits by supporting multi-resolution tiles. The scene is split into a number of slices, determined by the scroll root of each primitive, which can be found by the primitive's spatial node indices. If a scene contains too many slices, then picture caching is disabled on the page, to avoid excessive texture memory usage, and rendering falls back to rasterizing each frame. The specific changes in this patch are: * Support tile caches for multiple scroll roots, allowing the entire page (including fixed divs and the main UI bar) to be cached in most cases, in addition to the main content. * Remove requirement to read tiles back from the framebuffer. Instead, they are drawn into the picture cache target tiles, and blitted to the screen. This is slightly slower than the existing picture caching when content is constantly changing, however this cost will disappear / become irrelevant when the OS compositor integration work is complete. * Switch picture cache render targets to be nearest sampled (they are always rendered 1:1) and support depth buffer targets. * Make use of the external scroll offset support to allow removal of the primitive correlation hacks in the previous picture caching implementation. Also allows storing of primitive dependencies in picture space rather than world space, which reduces floating point inaccuracies. * Determine if each tile and picture cache can be considered opaque. This is used to determine whether subpixel AA text rendering is available on a slice, and for rendering optimizations related to disabling blending and/or tile clears. * Use the clip chain instance results from the recent visibility pass work to determine clip chain dependencies. This results in fewer clip item dependencies in tiles, which is faster to check validity and reduces redundant invalidations. * Remove extra overhead during batching related to batch lists, and region iteration, as they are no longer required. * Support PrimitiveVisibilityMask during batching. This allows a single traversal of a picture (surface) root during batching to efficiently construct multiple alpha batcher objects (typically one per invalida tile). * Picture caching is now handled implicitly by WR, depending on the content of the scene. There is no requirement for client code to manually select which stacking context should be cached. * Simplify how clip chain / transform dependencies are tracked by picture cache tiles. * Support pushing / popping enclosing clip chain roots without the need for a stacking context / picture in some cases. This simplifies the logic to split the scene into multiple slices. The main remaining work in this area is (a) extend the code to optionally provide each slice as an input to the OS compositor rather than drawing the tiles in WR, and (b) support multi-resolution tiles so that we reduce the draw call, batching and render target overhead in cases where much of the page content is changing. Differential Revision: https://phabricator.services.mozilla.com/D34319
gfx/layers/apz/test/reftest/reftest.list
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/clip.rs
gfx/wr/webrender/src/clip_scroll_tree.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/internal_types.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/prim_store/text_run.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/render_task.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/spatial_node.rs
gfx/wr/webrender/src/texture_cache.rs
gfx/wr/webrender/src/tiling.rs
gfx/wr/webrender/src/util.rs
gfx/wr/wrench/reftests/blend/reftest.list
gfx/wr/wrench/reftests/border/border-gradient-nine-patch.png
gfx/wr/wrench/reftests/border/degenerate-curve.png
gfx/wr/wrench/reftests/border/reftest.list
gfx/wr/wrench/reftests/boxshadow/box-shadow-huge-radius.png
gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-2.png
gfx/wr/wrench/reftests/boxshadow/box-shadow-large-blur-radius-3.png
gfx/wr/wrench/reftests/boxshadow/inset-subpx.png
gfx/wr/wrench/reftests/clip/clip-45-degree-rotation-ref.png
gfx/wr/wrench/reftests/clip/reftest.list
gfx/wr/wrench/reftests/filters/blend-clipped.png
gfx/wr/wrench/reftests/filters/filter-blur.png
gfx/wr/wrench/reftests/filters/filter-drop-shadow-on-viewport-edge.png
gfx/wr/wrench/reftests/filters/filter-drop-shadow.png
gfx/wr/wrench/reftests/filters/filter-large-blur-radius.png
gfx/wr/wrench/reftests/filters/reftest.list
gfx/wr/wrench/reftests/gradient/premultiplied-angle-2.png
gfx/wr/wrench/reftests/gradient/premultiplied-angle.png
gfx/wr/wrench/reftests/gradient/premultiplied-radial-2.png
gfx/wr/wrench/reftests/gradient/premultiplied-radial.png
gfx/wr/wrench/reftests/gradient/radial-circle-ref.png
gfx/wr/wrench/reftests/gradient/radial-ellipse-ref.png
gfx/wr/wrench/reftests/gradient/reftest.list
gfx/wr/wrench/reftests/gradient/repeat-border-radius.png
gfx/wr/wrench/reftests/image/yuv.png
gfx/wr/wrench/reftests/invalidation/reftest.list
gfx/wr/wrench/reftests/mask/reftest.list
gfx/wr/wrench/reftests/performance/reftest.list
gfx/wr/wrench/reftests/snap/preserve-3d.png
gfx/wr/wrench/reftests/snap/reftest.list
gfx/wr/wrench/reftests/split/reftest.list
gfx/wr/wrench/reftests/text/non-opaque-notref.yaml
gfx/wr/wrench/reftests/text/non-opaque.yaml
gfx/wr/wrench/reftests/text/reftest.list
gfx/wr/wrench/reftests/text/shadow-transforms.png
gfx/wr/wrench/reftests/transforms/border-zoom.png
gfx/wr/wrench/reftests/transforms/coord-system.png
gfx/wr/wrench/reftests/transforms/image-rotated-clip.png
gfx/wr/wrench/reftests/transforms/local-clip.png
gfx/wr/wrench/reftests/transforms/near-plane-clip.png
gfx/wr/wrench/reftests/transforms/perspective-clip.png
gfx/wr/wrench/reftests/transforms/perspective-mask.png
gfx/wr/wrench/reftests/transforms/perspective-origin.png
gfx/wr/wrench/reftests/transforms/perspective.png
gfx/wr/wrench/reftests/transforms/prim-suite.png
gfx/wr/wrench/reftests/transforms/reftest.list
gfx/wr/wrench/reftests/transforms/rotated-clip-large.png
gfx/wr/wrench/reftests/transforms/rotated-clip.png
gfx/wr/wrench/reftests/transforms/rotated-image.png
gfx/wr/wrench/reftests/transforms/screen-space-blit-trivial.png
gfx/wr/wrench/reftests/transforms/screen-space-blur.png
gfx/wr/wrench/src/args.yaml
gfx/wr/wrench/src/main.rs
gfx/wr/wrench/src/wrench.rs
layout/reftests/async-scrolling/reftest.list
layout/reftests/backgrounds/gradient/reftest.list
layout/reftests/border-image/reftest.list
layout/reftests/css-gradients/reftest.list
layout/reftests/text-shadow/reftest.list
layout/reftests/transform-3d/reftest.list
layout/reftests/w3c-css/submitted/values3/reftest.list
testing/web-platform/meta/css/motion/offset-rotate-005.html.ini
--- a/gfx/layers/apz/test/reftest/reftest.list
+++ b/gfx/layers/apz/test/reftest/reftest.list
@@ -14,17 +14,18 @@ fuzzy-if(Android,0-54,0-20) skip-if(!And
 fuzzy-if(Android,0-45,0-27) skip-if(!Android) pref(apz.allow_zooming,true) == async-scrollbar-zoom-2.html async-scrollbar-zoom-2-ref.html
 
 # Test scrollbars working properly with pinch-zooming, i.e. different document resolutions.
 # As above, the end of the scrollthumb won't match perfectly, but the bulk of the scrollbar should be present and identical.
 # On desktop, even more fuzz is needed because thumb scaling is not exactly proportional: making the page twice as long 
 # won't make the thumb exactly twice as short, which is what the test expects. That's fine, as the purpose of the test is
 # to catch more fundamental scrollbar rendering bugs such as the entire track being mispositioned or the thumb being
 # clipped away.
-fuzzy-if(Android,0-54,0-14) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == scrollbar-zoom-resolution-1.html scrollbar-zoom-resolution-1-ref.html
+# Fails on WebRender - seems related to the viewport_rect in scroll frames not being scaled correctly.
+fuzzy-if(Android,0-54,0-14) fuzzy-if(!Android,0-128,0-137) fails-if(webrender) pref(apz.allow_zooming,true) == scrollbar-zoom-resolution-1.html scrollbar-zoom-resolution-1-ref.html
 fuzzy-if(Android,0-51,0-22) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == scrollbar-zoom-resolution-2.html scrollbar-zoom-resolution-2-ref.html
 
 # Meta-viewport tag support
 skip-if(!Android) pref(apz.allow_zooming,true) == initial-scale-1.html initial-scale-1-ref.html
 
 skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping-ref.html
 
 # Test that position:fixed and position:sticky elements are attached to the
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -10,23 +10,23 @@ use crate::clip_scroll_tree::{ClipScroll
 use crate::glyph_rasterizer::GlyphFormat;
 use crate::gpu_cache::{GpuBlockData, GpuCache, GpuCacheHandle, GpuCacheAddress};
 use crate::gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders, ZBufferId, ZBufferIdGenerator};
 use crate::gpu_types::{ClipMaskInstance, SplitCompositeInstance, SnapOffsets};
 use crate::gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use crate::gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
 use crate::internal_types::{FastHashMap, SavedTargetIndex, TextureSource, Filter};
 use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
-use crate::prim_store::{DeferredResolve, EdgeAaSegmentMask, PrimitiveInstanceKind, PrimitiveVisibilityIndex};
+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};
 use crate::prim_store::{recompute_snap_offsets};
 use crate::prim_store::image::ImageSource;
 use crate::render_backend::DataStores;
-use crate::render_task::{RenderTaskAddress, RenderTaskId, RenderTaskGraph, TileBlit};
+use crate::render_task::{RenderTaskAddress, RenderTaskId, RenderTaskGraph};
 use crate::renderer::{BlendMode, ImageBufferKind, ShaderColorMode};
 use crate::renderer::{BLOCKS_PER_UV_RECT, MAX_VERTEX_TEXTURE_WIDTH};
 use crate::resource_cache::{CacheItem, GlyphFetchResult, ImageRequest, ResourceCache, ImageProperties};
 use smallvec::SmallVec;
 use std::{f32, i32, usize};
 use crate::tiling::{RenderTargetContext};
 use crate::util::{project_rect, TransformedRectKind};
 
@@ -300,42 +300,159 @@ impl OpaqueBatchList {
         //           build these in reverse and avoid having
         //           to reverse the instance array here.
         for batch in &mut self.batches {
             batch.instances.reverse();
         }
     }
 }
 
-pub struct BatchList {
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveBatch {
+    pub key: BatchKey,
+    pub instances: Vec<PrimitiveInstanceData>,
+}
+
+impl PrimitiveBatch {
+    fn new(key: BatchKey) -> PrimitiveBatch {
+        PrimitiveBatch {
+            key,
+            instances: Vec::new(),
+        }
+    }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AlphaBatchContainer {
+    pub opaque_batches: Vec<PrimitiveBatch>,
+    pub alpha_batches: Vec<PrimitiveBatch>,
+    /// The overall scissor rect for this render task, if one
+    /// is required.
+    pub task_scissor_rect: Option<DeviceIntRect>,
+    /// The rectangle of the owning render target that this
+    /// set of batches affects.
+    pub task_rect: DeviceIntRect,
+}
+
+impl AlphaBatchContainer {
+    pub fn new(
+        task_scissor_rect: Option<DeviceIntRect>,
+    ) -> AlphaBatchContainer {
+        AlphaBatchContainer {
+            opaque_batches: Vec::new(),
+            alpha_batches: Vec::new(),
+            task_scissor_rect,
+            task_rect: DeviceIntRect::zero(),
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.opaque_batches.is_empty() &&
+        self.alpha_batches.is_empty()
+    }
+
+    fn merge(&mut self, builder: AlphaBatchBuilder, task_rect: &DeviceIntRect) {
+        self.task_rect = self.task_rect.union(task_rect);
+
+        for other_batch in builder.opaque_batch_list.batches {
+            let batch_index = self.opaque_batches.iter().position(|batch| {
+                batch.key.is_compatible_with(&other_batch.key)
+            });
+
+            match batch_index {
+                Some(batch_index) => {
+                    self.opaque_batches[batch_index].instances.extend(other_batch.instances);
+                }
+                None => {
+                    self.opaque_batches.push(other_batch);
+                }
+            }
+        }
+
+        let mut min_batch_index = 0;
+
+        for other_batch in builder.alpha_batch_list.batches {
+            let batch_index = self.alpha_batches.iter().skip(min_batch_index).position(|batch| {
+                batch.key.is_compatible_with(&other_batch.key)
+            });
+
+            match batch_index {
+                Some(batch_index) => {
+                    let batch_index = batch_index + min_batch_index;
+                    self.alpha_batches[batch_index].instances.extend(other_batch.instances);
+                    min_batch_index = batch_index;
+                }
+                None => {
+                    self.alpha_batches.push(other_batch);
+                    min_batch_index = self.alpha_batches.len();
+                }
+            }
+        }
+    }
+}
+
+/// Each segment can optionally specify a per-segment
+/// texture set and one user data field.
+#[derive(Debug, Copy, Clone)]
+struct SegmentInstanceData {
+    textures: BatchTextures,
+    user_data: i32,
+}
+
+/// Encapsulates the logic of building batches for items that are blended.
+pub struct AlphaBatchBuilder {
     pub alpha_batch_list: AlphaBatchList,
     pub opaque_batch_list: OpaqueBatchList,
-    /// A list of rectangle regions this batch should be drawn
-    /// in. Each region will have scissor rect set before drawing.
-    pub regions: Vec<DeviceIntRect>,
-    pub tile_blits: Vec<TileBlit>,
+    pub render_task_id: RenderTaskId,
+    render_task_address: RenderTaskAddress,
+    pub vis_mask: PrimitiveVisibilityMask,
 }
 
-impl BatchList {
+impl AlphaBatchBuilder {
     pub fn new(
         screen_size: DeviceIntSize,
-        regions: Vec<DeviceIntRect>,
-        tile_blits: Vec<TileBlit>,
         break_advanced_blend_batches: bool,
         lookback_count: usize,
+        render_task_id: RenderTaskId,
+        render_task_address: RenderTaskAddress,
+        vis_mask: PrimitiveVisibilityMask,
     ) -> Self {
         // The threshold for creating a new batch is
         // one quarter the screen size.
         let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
 
-        BatchList {
+        AlphaBatchBuilder {
             alpha_batch_list: AlphaBatchList::new(break_advanced_blend_batches, lookback_count),
             opaque_batch_list: OpaqueBatchList::new(batch_area_threshold, lookback_count),
-            regions,
-            tile_blits,
+            render_task_id,
+            render_task_address,
+            vis_mask,
+        }
+    }
+
+    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();
+
+        if task_scissor_rect.is_none() {
+            merged_batches.merge(self, &task_rect);
+        } else {
+            batch_containers.push(AlphaBatchContainer {
+                alpha_batches: self.alpha_batch_list.batches,
+                opaque_batches: self.opaque_batch_list.batches,
+                task_scissor_rect,
+                task_rect,
+            });
         }
     }
 
     pub fn push_single_instance(
         &mut self,
         key: BatchKey,
         bounding_rect: &PictureRect,
         z_id: ZBufferId,
@@ -363,301 +480,104 @@ impl BatchList {
             BlendMode::SubpixelWithBgColor |
             BlendMode::SubpixelDualSource |
             BlendMode::Advanced(_) => {
                 self.alpha_batch_list
                     .set_params_and_get_batch(key, bounding_rect, z_id)
             }
         }
     }
-
-    fn finalize(&mut self) {
-        self.opaque_batch_list.finalize()
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct PrimitiveBatch {
-    pub key: BatchKey,
-    pub instances: Vec<PrimitiveInstanceData>,
-}
-
-impl PrimitiveBatch {
-    fn new(key: BatchKey) -> PrimitiveBatch {
-        PrimitiveBatch {
-            key,
-            instances: Vec::new(),
-        }
-    }
-}
-
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct AlphaBatchContainer {
-    pub opaque_batches: Vec<PrimitiveBatch>,
-    pub alpha_batches: Vec<PrimitiveBatch>,
-    /// The overall scissor rect for this render task, if one
-    /// is required.
-    pub task_scissor_rect: Option<DeviceIntRect>,
-    /// A list of rectangle regions this batch should be drawn
-    /// in. Each region will have scissor rect set before drawing.
-    pub regions: Vec<DeviceIntRect>,
-    pub tile_blits: Vec<TileBlit>,
-    /// The rectangle of the owning render target that this
-    /// set of batches affects.
-    pub task_rect: DeviceIntRect,
-}
-
-impl AlphaBatchContainer {
-    pub fn new(
-        task_scissor_rect: Option<DeviceIntRect>,
-        regions: Vec<DeviceIntRect>,
-    ) -> AlphaBatchContainer {
-        AlphaBatchContainer {
-            opaque_batches: Vec::new(),
-            alpha_batches: Vec::new(),
-            task_scissor_rect,
-            regions,
-            tile_blits: Vec::new(),
-            task_rect: DeviceIntRect::zero(),
-        }
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.opaque_batches.is_empty() &&
-        self.alpha_batches.is_empty()
-    }
-
-    fn merge(&mut self, batch_list: BatchList, task_rect: &DeviceIntRect) {
-        self.task_rect = self.task_rect.union(task_rect);
-
-        for other_batch in batch_list.opaque_batch_list.batches {
-            let batch_index = self.opaque_batches.iter().position(|batch| {
-                batch.key.is_compatible_with(&other_batch.key)
-            });
-
-            match batch_index {
-                Some(batch_index) => {
-                    self.opaque_batches[batch_index].instances.extend(other_batch.instances);
-                }
-                None => {
-                    self.opaque_batches.push(other_batch);
-                }
-            }
-        }
-
-        let mut min_batch_index = 0;
-
-        for other_batch in batch_list.alpha_batch_list.batches {
-            let batch_index = self.alpha_batches.iter().skip(min_batch_index).position(|batch| {
-                batch.key.is_compatible_with(&other_batch.key)
-            });
-
-            match batch_index {
-                Some(batch_index) => {
-                    let batch_index = batch_index + min_batch_index;
-                    self.alpha_batches[batch_index].instances.extend(other_batch.instances);
-                    min_batch_index = batch_index;
-                }
-                None => {
-                    self.alpha_batches.push(other_batch);
-                    min_batch_index = self.alpha_batches.len();
-                }
-            }
-        }
-    }
-}
-
-/// Each segment can optionally specify a per-segment
-/// texture set and one user data field.
-#[derive(Debug, Copy, Clone)]
-struct SegmentInstanceData {
-    textures: BatchTextures,
-    user_data: i32,
-}
-
-/// Encapsulates the logic of building batches for items that are blended.
-pub struct AlphaBatchBuilder {
-    pub batch_lists: Vec<BatchList>,
-    screen_size: DeviceIntSize,
-    break_advanced_blend_batches: bool,
-    lookback_count: usize,
-    render_task_id: RenderTaskId,
-    render_task_address: RenderTaskAddress,
-}
-
-impl AlphaBatchBuilder {
-    pub fn new(
-        screen_size: DeviceIntSize,
-        break_advanced_blend_batches: bool,
-        lookback_count: usize,
-        render_task_id: RenderTaskId,
-        render_task_address: RenderTaskAddress,
-    ) -> Self {
-        let batch_lists = vec![
-            BatchList::new(
-                screen_size,
-                Vec::new(),
-                Vec::new(),
-                break_advanced_blend_batches,
-                lookback_count,
-            ),
-        ];
-
-        AlphaBatchBuilder {
-            batch_lists,
-            screen_size,
-            break_advanced_blend_batches,
-            lookback_count,
-            render_task_id,
-            render_task_address,
-        }
-    }
-
-    fn push_new_batch_list(
-        &mut self,
-        regions: Vec<DeviceIntRect>,
-        tile_blits: Vec<TileBlit>,
-    ) {
-        self.batch_lists.push(BatchList::new(
-            self.screen_size,
-            regions,
-            tile_blits,
-            self.break_advanced_blend_batches,
-            self.lookback_count,
-        ));
-    }
-
-    fn current_batch_list(&mut self) -> &mut BatchList {
-        self.batch_lists.last_mut().unwrap()
-    }
-
-    fn can_merge(&self) -> bool {
-        self.batch_lists.len() == 1
-    }
-
-    pub fn build(
-        mut self,
-        batch_containers: &mut Vec<AlphaBatchContainer>,
-        merged_batches: &mut AlphaBatchContainer,
-        task_rect: DeviceIntRect,
-        task_scissor_rect: Option<DeviceIntRect>,
-    ) {
-        for batch_list in &mut self.batch_lists {
-            batch_list.finalize();
-        }
-
-        if task_scissor_rect.is_none() && self.can_merge() {
-            let batch_list = self.batch_lists.pop().unwrap();
-            debug_assert!(batch_list.tile_blits.is_empty());
-            merged_batches.merge(batch_list, &task_rect);
-        } else {
-            for batch_list in self.batch_lists {
-                batch_containers.push(AlphaBatchContainer {
-                    alpha_batches: batch_list.alpha_batch_list.batches,
-                    opaque_batches: batch_list.opaque_batch_list.batches,
-                    task_scissor_rect,
-                    regions: batch_list.regions,
-                    tile_blits: batch_list.tile_blits,
-                    task_rect,
-                });
-            }
-        }
-    }
 }
 
 /// Supports (recursively) adding a list of primitives and pictures to an alpha batch
 /// builder. In future, it will support multiple dirty regions / slices, allowing the
 /// contents of a picture to be spliced into multiple batch builders.
 pub struct BatchBuilder {
     /// A temporary buffer that is used during glyph fetching, stored here
     /// to reduce memory allocations.
     glyph_fetch_buffer: Vec<GlyphFetchResult>,
 
-    /// The batchers that primitives will be added to as the
-    /// picture tree is traversed.
-    batcher: AlphaBatchBuilder,
+    pub batchers: Vec<AlphaBatchBuilder>,
 }
 
 impl BatchBuilder {
-    pub fn new(
-        batcher: AlphaBatchBuilder,
-    ) -> Self {
+    pub fn new(batchers: Vec<AlphaBatchBuilder>) -> Self {
         BatchBuilder {
             glyph_fetch_buffer: Vec::new(),
-            batcher,
+            batchers,
         }
     }
 
-    pub fn finalize(self) -> AlphaBatchBuilder {
-        self.batcher
+    pub fn finalize(self) -> Vec<AlphaBatchBuilder> {
+        self.batchers
     }
 
     fn add_brush_instance_to_batches(
         &mut self,
         batch_key: BatchKey,
         bounding_rect: &PictureRect,
         z_id: ZBufferId,
         segment_index: i32,
         edge_flags: EdgeAaSegmentMask,
         clip_task_address: RenderTaskAddress,
         brush_flags: BrushFlags,
         prim_header_index: PrimitiveHeaderIndex,
         user_data: i32,
+        prim_vis_mask: PrimitiveVisibilityMask,
     ) {
-        // TODO(gw): In future, this will be a loop adding the primitive
-        //           to multiple batch list(s), depending on the primitive
-        //           visibility mask.
-
-        let render_task_address = self.batcher.render_task_address;
+        for batcher in &mut self.batchers {
+            if batcher.vis_mask.intersects(prim_vis_mask) {
+                let render_task_address = batcher.render_task_address;
 
-        let instance = BrushInstance {
-            segment_index,
-            edge_flags,
-            clip_task_address,
-            render_task_address,
-            brush_flags,
-            prim_header_index,
-            user_data,
-        };
+                let instance = BrushInstance {
+                    segment_index,
+                    edge_flags,
+                    clip_task_address,
+                    render_task_address,
+                    brush_flags,
+                    prim_header_index,
+                    user_data,
+                };
 
-        self.batcher.current_batch_list().push_single_instance(
-            batch_key,
-            bounding_rect,
-            z_id,
-            PrimitiveInstanceData::from(instance),
-        );
+                batcher.push_single_instance(
+                    batch_key,
+                    bounding_rect,
+                    z_id,
+                    PrimitiveInstanceData::from(instance),
+                );
+            }
+        }
     }
 
     fn add_split_composite_instance_to_batches(
         &mut self,
         batch_key: BatchKey,
         bounding_rect: &PictureRect,
         z_id: ZBufferId,
         prim_header_index: PrimitiveHeaderIndex,
         polygons_address: GpuCacheAddress,
+        prim_vis_mask: PrimitiveVisibilityMask,
     ) {
-        // TODO(gw): In future, this will be a loop adding the primitive
-        //           to multiple batch list(s), depending on the primitive
-        //           visibility mask.
-
-        let render_task_address = self.batcher.render_task_address;
+        for batcher in &mut self.batchers {
+            if batcher.vis_mask.intersects(prim_vis_mask) {
+                let render_task_address = batcher.render_task_address;
 
-        self.batcher.current_batch_list().push_single_instance(
-            batch_key,
-            bounding_rect,
-            z_id,
-            PrimitiveInstanceData::from(SplitCompositeInstance {
-                prim_header_index,
-                render_task_address,
-                polygons_address,
-                z: z_id,
-            }),
-        );
+                batcher.push_single_instance(
+                    batch_key,
+                    bounding_rect,
+                    z_id,
+                    PrimitiveInstanceData::from(SplitCompositeInstance {
+                        prim_header_index,
+                        render_task_address,
+                        polygons_address,
+                        z: z_id,
+                    }),
+                );
+            }
+        }
     }
 
     /// Add a picture to a given batch builder.
     pub fn add_pic_to_batch(
         &mut self,
         pic: &PicturePrimitive,
         ctx: &RenderTargetContext,
         gpu_cache: &mut GpuCache,
@@ -730,27 +650,31 @@ impl BatchBuilder {
 
         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,
         );
 
         let snap_offsets = prim_info.snap_offsets;
+        let prim_vis_mask = prim_info.visibility_mask;
 
         if is_chased {
             println!("\tbatch {:?} with bound {:?}", prim_rect, bounding_rect);
         }
 
         if !bounding_rect.is_empty() {
             debug_assert_eq!(prim_info.clip_chain.pic_spatial_node_index, surface_spatial_node_index,
                 "The primitive's bounding box is specified in a different coordinate system from the current batch!");
         }
 
         match prim_instance.kind {
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain => {}
+
             PrimitiveInstanceKind::Clear { data_handle } => {
                 let prim_data = &ctx.data_stores.prim[data_handle];
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
 
                 // TODO(gw): We can abstract some of the common code below into
                 //           helper methods, as we port more primitives to make
                 //           use of interning.
 
@@ -784,16 +708,17 @@ impl BatchBuilder {
                     bounding_rect,
                     z_id,
                     INVALID_SEGMENT_INDEX,
                     EdgeAaSegmentMask::all(),
                     clip_task_address,
                     BrushFlags::PERSPECTIVE_INTERPOLATION,
                     prim_header_index,
                     0,
+                    prim_vis_mask,
                 );
             }
             PrimitiveInstanceKind::NormalBorder { data_handle, ref cache_handles, .. } => {
                 let prim_data = &ctx.data_stores.normal_border[data_handle];
                 let common_data = &prim_data.common;
                 let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
                 let cache_handles = &ctx.scratch.border_cache_handles[*cache_handles];
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
@@ -857,16 +782,17 @@ impl BatchBuilder {
                     specified_blend_mode,
                     non_segmented_blend_mode,
                     prim_header_index,
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_info.clip_task_index,
+                    prim_vis_mask,
                     ctx,
                 );
             }
             PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
                 let run = &ctx.prim_store.text_runs[run_index];
                 let subpx_dir = run.used_font.get_subpx_dir();
 
                 // The GPU cache data is stored in the template and reused across
@@ -901,20 +827,17 @@ impl BatchBuilder {
                         (run.reference_frame_relative_offset.y * 256.0) as i32,
                         (raster_scale * 65535.0).round() as i32,
                         clip_task_address.0 as i32,
                     ],
                 );
                 let base_instance = GlyphInstance::new(
                     prim_header_index,
                 );
-                let alpha_batch_list = &mut self.batcher.batch_lists.last_mut().unwrap().alpha_batch_list;
-                let render_task_address = render_tasks.get_task_address(
-                    self.batcher.render_task_id,
-                );
+                let batchers = &mut self.batchers;
 
                 ctx.resource_cache.fetch_glyphs(
                     run.used_font.clone(),
                     &glyph_keys,
                     &mut self.glyph_fetch_buffer,
                     gpu_cache,
                     |texture_id, mut glyph_format, glyphs| {
                         debug_assert_ne!(texture_id, TextureSource::Invalid);
@@ -973,30 +896,36 @@ impl BatchBuilder {
                                 (
                                     BlendMode::PremultipliedAlpha,
                                     ShaderColorMode::ColorBitmap,
                                 )
                             }
                         };
 
                         let key = BatchKey::new(kind, blend_mode, textures);
-                        let batch = alpha_batch_list.set_params_and_get_batch(
-                            key,
-                            bounding_rect,
-                            z_id,
-                        );
+
+                        for batcher in batchers.iter_mut() {
+                            if batcher.vis_mask.intersects(prim_vis_mask) {
+                                let render_task_address = batcher.render_task_address;
+                                let batch = batcher.alpha_batch_list.set_params_and_get_batch(
+                                    key,
+                                    bounding_rect,
+                                    z_id,
+                                );
 
-                        for glyph in glyphs {
-                            batch.push(base_instance.build(
-                                glyph.index_in_text_run | ((render_task_address.0 as i32) << 16),
-                                glyph.uv_rect_address.as_int(),
-                                (rasterization_space as i32) << 16 |
-                                (subpx_dir as u32 as i32) << 8 |
-                                (color_mode as u32 as i32),
-                            ));
+                                for glyph in glyphs {
+                                    batch.push(base_instance.build(
+                                        glyph.index_in_text_run | ((render_task_address.0 as i32) << 16),
+                                        glyph.uv_rect_address.as_int(),
+                                        (rasterization_space as i32) << 16 |
+                                        (subpx_dir as u32 as i32) << 8 |
+                                        (color_mode as u32 as i32),
+                                    ));
+                                }
+                            }
                         }
                     },
                 );
             }
             PrimitiveInstanceKind::LineDecoration { data_handle, ref cache_handle, .. } => {
                 // The GPU cache data is stored in the template and reused across
                 // frames and display lists.
                 let common_data = &ctx.data_stores.line_decoration[data_handle].common;
@@ -1075,16 +1004,17 @@ impl BatchBuilder {
                     bounding_rect,
                     z_id,
                     INVALID_SEGMENT_INDEX,
                     EdgeAaSegmentMask::all(),
                     clip_task_address,
                     BrushFlags::PERSPECTIVE_INTERPOLATION,
                     prim_header_index,
                     segment_user_data,
+                    prim_vis_mask,
                 );
             }
             PrimitiveInstanceKind::Picture { pic_index, segment_instance_index, .. } => {
                 let picture = &ctx.prim_store.pictures[pic_index.0];
                 let non_segmented_blend_mode = BlendMode::PremultipliedAlpha;
                 let prim_cache_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
 
                 let prim_header = PrimitiveHeader {
@@ -1099,28 +1029,17 @@ impl BatchBuilder {
                     // Convert all children of the 3D hierarchy root into batches.
                     Picture3DContext::In { root_data: Some(ref list), .. } => {
                         for child in list {
                             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 child_prim_instance.kind {
                                 PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
-                                PrimitiveInstanceKind::LineDecoration { .. } |
-                                PrimitiveInstanceKind::TextRun { .. } |
-                                PrimitiveInstanceKind::NormalBorder { .. } |
-                                PrimitiveInstanceKind::ImageBorder { .. } |
-                                PrimitiveInstanceKind::Rectangle { .. } |
-                                PrimitiveInstanceKind::YuvImage { .. } |
-                                PrimitiveInstanceKind::Image { .. } |
-                                PrimitiveInstanceKind::LinearGradient { .. } |
-                                PrimitiveInstanceKind::RadialGradient { .. } |
-                                PrimitiveInstanceKind::Clear { .. } => {
-                                    unreachable!();
-                                }
+                                _ => unreachable!(),
                             };
                             let pic = &ctx.prim_store.pictures[child_pic_index.0];
 
                             // Get clip task, if set, for the picture primitive.
                             let clip_task_address = ctx.get_prim_clip_task_address(
                                 child_prim_info.clip_task_index,
                                 render_tasks,
                             ).unwrap_or(OPAQUE_TASK_ADDRESS);
@@ -1164,16 +1083,17 @@ impl BatchBuilder {
                             );
 
                             self.add_split_composite_instance_to_batches(
                                 key,
                                 &prim_info.clip_chain.pic_clip_rect,
                                 z_id,
                                 prim_header_index,
                                 child.gpu_address,
+                                prim_info.visibility_mask,
                             );
                         }
                     }
                     // 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,
                     // Proceed for non-3D pictures.
                     Picture3DContext::Out => ()
@@ -1196,177 +1116,81 @@ impl BatchBuilder {
 
                         let surface = &ctx.surfaces[raster_config.surface_index.0];
                         let surface_task = surface.render_tasks.map(|s| s.root);
 
                         match raster_config.composite_mode {
                             PictureCompositeMode::TileCache { .. } => {
                                 let tile_cache = picture.tile_cache.as_ref().unwrap();
 
-                                // If the tile cache is disabled, just recurse into the
-                                // picture like a normal pass-through picture, adding
-                                // any child primitives into the parent surface batches.
-                                if !tile_cache.is_enabled {
-                                    // Forcefully break the batches if the
-                                    if surface.surface_spatial_node_index != surface_spatial_node_index {
-                                        self.batcher.push_new_batch_list(
-                                            Vec::new(),
-                                            Vec::new(),
-                                        );
+                                let tile_clip_rect = match tile_cache.local_rect.intersection(&tile_cache.local_clip_rect) {
+                                    Some(rect) => rect,
+                                    None => {
+                                        return;
                                     }
+                                };
+                                let local_tile_clip_rect = LayoutRect::from_untyped(&tile_clip_rect.to_untyped());
 
-                                    self.add_pic_to_batch(
-                                        picture,
-                                        ctx,
-                                        gpu_cache,
-                                        render_tasks,
-                                        deferred_resolves,
-                                        prim_headers,
-                                        transforms,
-                                        root_spatial_node_index,
-                                        surface.surface_spatial_node_index,
-                                        z_generator,
-                                    );
-
-                                    if surface.surface_spatial_node_index != surface_spatial_node_index {
-                                        self.batcher.push_new_batch_list(
-                                            Vec::new(),
-                                            Vec::new(),
-                                        );
-                                    }
-
-                                    return;
-                                }
+                                for key in &tile_cache.tiles_to_draw {
+                                    let tile = &tile_cache.tiles[key];
 
-                                // Construct a local clip rect that ensures we only draw pixels where
-                                // the local bounds of the picture extend to within the edge tiles.
-                                let local_clip_rect = prim_info
-                                    .combined_local_clip_rect
-                                    .intersection(&picture.snapped_local_rect)
-                                    .and_then(|rect| {
-                                        rect.intersection(&tile_cache.local_clip_rect)
-                                    });
-
-                                if let Some(local_clip_rect) = local_clip_rect {
-                                    // Step through each tile in the cache, and draw it with an image
-                                    // brush primitive if visible.
-
-                                    let kind = BatchKind::Brush(
-                                        BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
-                                    );
-
-                                    for tile_index in &tile_cache.tiles_to_draw {
-                                        let tile = &tile_cache.tiles[tile_index.0];
+                                    debug_assert!(tile.is_valid);
+                                    let local_tile_rect = LayoutRect::from_untyped(&tile.rect.to_untyped());
+                                    let prim_header = PrimitiveHeader {
+                                        local_rect: local_tile_rect,
+                                        local_clip_rect: local_tile_clip_rect,
+                                        snap_offsets: SnapOffsets::empty(),
+                                        specific_prim_address: prim_cache_address,
+                                        transform_id,
+                                    };
 
-                                        // Get the local rect of the tile.
-                                        let tile_rect = tile.local_rect;
-
-                                        // Adjust the snap offsets for the tile.
-                                        let snap_offsets = recompute_snap_offsets(
-                                            tile_rect,
-                                            prim_rect,
-                                            snap_offsets,
-                                        );
+                                    let (opacity, blend_mode) = if tile.is_opaque || tile_cache.is_opaque() {
+                                        (PrimitiveOpacity::opaque(), BlendMode::None)
+                                    } else {
+                                        (PrimitiveOpacity::translucent(), BlendMode::PremultipliedAlpha)
+                                    };
 
-                                        let prim_header = PrimitiveHeader {
-                                            local_rect: tile_rect,
-                                            local_clip_rect,
-                                            snap_offsets,
-                                            specific_prim_address: prim_cache_address,
-                                            transform_id,
-                                        };
-
-                                        let prim_header_index = prim_headers.push(&prim_header, z_id, [
+                                    let cache_item = ctx.resource_cache.texture_cache.get(&tile.handle);
+                                    let uv_rect_address = gpu_cache
+                                        .get_address(&cache_item.uv_rect_handle)
+                                        .as_int();
+                                    let textures = BatchTextures::color(cache_item.texture_id);
+                                    let batch_params = BrushBatchParameters::shared(
+                                        BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
+                                        textures,
+                                        [
                                             ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
                                             RasterizationSpace::Local as i32,
                                             get_shader_opacity(1.0),
                                             0,
-                                        ]);
-
-                                        let cache_item = ctx
-                                            .resource_cache
-                                            .get_texture_cache_item(&tile.handle);
-
-                                        let key = BatchKey::new(
-                                            kind,
-                                            BlendMode::None,
-                                            BatchTextures::color(cache_item.texture_id),
-                                        );
-
-                                        let uv_rect_address = gpu_cache
-                                            .get_address(&cache_item.uv_rect_handle)
-                                            .as_int();
+                                        ],
+                                        uv_rect_address,
+                                    );
 
-                                        self.add_brush_instance_to_batches(
-                                            key,
-                                            bounding_rect,
-                                            z_id,
-                                            INVALID_SEGMENT_INDEX,
-                                            EdgeAaSegmentMask::empty(),
-                                            clip_task_address,
-                                            brush_flags,
-                                            prim_header_index,
-                                            uv_rect_address,
-                                        );
-                                    }
-
-                                    // If there is a dirty rect for the tile cache, recurse into the
-                                    // main picture primitive list, and draw them first.
-                                    if !tile_cache.dirty_region.is_empty() {
-                                        let mut tile_blits = Vec::new();
-
-                                        let (target_rect, _) = render_tasks[self.batcher.render_task_id]
-                                            .get_target_rect();
+                                    let prim_header_index = prim_headers.push(
+                                        &prim_header,
+                                        z_id,
+                                        batch_params.prim_user_data,
+                                    );
 
-                                        for blit in &tile_cache.pending_blits {
-                                            tile_blits.push(TileBlit {
-                                                dest_offset: blit.dest_offset,
-                                                size: blit.size,
-                                                target: blit.target.clone(),
-                                                src_offset: DeviceIntPoint::new(
-                                                    blit.src_offset.x + target_rect.origin.x,
-                                                    blit.src_offset.y + target_rect.origin.y,
-                                                ),
-                                            })
-                                        }
-
-                                        // Collect the list of regions to scissor and repeat
-                                        // the draw calls into, based on dirty rects.
-                                        let batch_regions = tile_cache
-                                            .dirty_region
-                                            .dirty_rects
-                                            .iter()
-                                            .map(|dirty_rect| {
-                                                (dirty_rect.world_rect * ctx.global_device_pixel_scale).round().to_i32()
-                                            })
-                                            .collect();
-
-                                        self.batcher.push_new_batch_list(
-                                            batch_regions,
-                                            tile_blits,
-                                        );
-
-                                        self.add_pic_to_batch(
-                                            picture,
-                                            ctx,
-                                            gpu_cache,
-                                            render_tasks,
-                                            deferred_resolves,
-                                            prim_headers,
-                                            transforms,
-                                            root_spatial_node_index,
-                                            surface.surface_spatial_node_index,
-                                            z_generator,
-                                        );
-
-                                        self.batcher.push_new_batch_list(
-                                            Vec::new(),
-                                            Vec::new(),
-                                        );
-                                    }
+                                    self.add_segmented_prim_to_batch(
+                                        None,
+                                        opacity,
+                                        &batch_params,
+                                        blend_mode,
+                                        blend_mode,
+                                        prim_header_index,
+                                        bounding_rect,
+                                        transform_kind,
+                                        render_tasks,
+                                        z_id,
+                                        prim_info.clip_task_index,
+                                        prim_vis_mask,
+                                        ctx,
+                                    );
                                 }
                             }
                             PictureCompositeMode::Filter(ref filter) => {
                                 assert!(filter.is_visible());
                                 match filter {
                                     Filter::Blur(..) => {
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
@@ -1392,16 +1216,17 @@ impl BatchBuilder {
                                             bounding_rect,
                                             z_id,
                                             INVALID_SEGMENT_INDEX,
                                             EdgeAaSegmentMask::empty(),
                                             clip_task_address,
                                             brush_flags,
                                             prim_header_index,
                                             uv_rect_address.as_int(),
+                                            prim_vis_mask,
                                         );
                                     }
                                     Filter::DropShadows(shadows) => {
                                         // Draw an instance per shadow first, following by the content.
 
                                         // The shadows and the content get drawn as a brush image.
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray),
@@ -1462,16 +1287,17 @@ impl BatchBuilder {
                                                 bounding_rect,
                                                 z_id,
                                                 INVALID_SEGMENT_INDEX,
                                                 EdgeAaSegmentMask::empty(),
                                                 clip_task_address,
                                                 brush_flags,
                                                 shadow_prim_header_index,
                                                 shadow_uv_rect_address,
+                                                prim_vis_mask,
                                             );
                                         }
                                         let z_id_content = z_generator.next();
 
                                         let content_prim_header_index = prim_headers.push(&prim_header, z_id_content, [
                                             ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
                                             RasterizationSpace::Screen as i32,
                                             get_shader_opacity(1.0),
@@ -1483,16 +1309,17 @@ impl BatchBuilder {
                                             bounding_rect,
                                             z_id_content,
                                             INVALID_SEGMENT_INDEX,
                                             EdgeAaSegmentMask::empty(),
                                             clip_task_address,
                                             brush_flags,
                                             content_prim_header_index,
                                             content_uv_rect_address,
+                                            prim_vis_mask,
                                         );
                                     }
                                     _ => {
                                         let filter_mode = match filter {
                                             Filter::Identity => 1, // matches `Contrast(1)`
                                             Filter::Blur(..) => 0,
                                             Filter::Contrast(..) => 1,
                                             Filter::Grayscale(..) => 2,
@@ -1562,16 +1389,17 @@ impl BatchBuilder {
                                             bounding_rect,
                                             z_id,
                                             INVALID_SEGMENT_INDEX,
                                             EdgeAaSegmentMask::empty(),
                                             clip_task_address,
                                             brush_flags,
                                             prim_header_index,
                                             0,
+                                            prim_vis_mask,
                                         );
                                     }
                                 }
                             }
                             PictureCompositeMode::ComponentTransferFilter(handle) => {
                                 // This is basically the same as the general filter case above
                                 // except we store a little more data in the filter mode and
                                 // a gpu cache handle in the user data.
@@ -1607,16 +1435,17 @@ impl BatchBuilder {
                                     bounding_rect,
                                     z_id,
                                     INVALID_SEGMENT_INDEX,
                                     EdgeAaSegmentMask::empty(),
                                     clip_task_address,
                                     brush_flags,
                                     prim_header_index,
                                     0,
+                                    prim_vis_mask,
                                 );
                             }
                             PictureCompositeMode::MixBlend(mode) if ctx.use_advanced_blending => {
                                 let (uv_rect_address, textures) = render_tasks.resolve_surface(
                                     surface_task.expect("bug: surface must be allocated by now"),
                                     gpu_cache,
                                 );
                                 let key = BatchKey::new(
@@ -1638,26 +1467,32 @@ impl BatchBuilder {
                                     bounding_rect,
                                     z_id,
                                     INVALID_SEGMENT_INDEX,
                                     EdgeAaSegmentMask::empty(),
                                     clip_task_address,
                                     brush_flags,
                                     prim_header_index,
                                     uv_rect_address.as_int(),
+                                    prim_vis_mask,
                                 );
                             }
                             PictureCompositeMode::MixBlend(mode) => {
                                 let cache_task_id = surface_task.expect("bug: surface must be allocated by now");
                                 let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
 
+                                // TODO(gw): For now, mix-blend is not supported as a picture
+                                //           caching root, so we can safely assume there is
+                                //           only a single batcher present.
+                                assert_eq!(self.batchers.len(), 1);
+
                                 let key = BatchKey::new(
                                     BatchKind::Brush(
                                         BrushBatchKind::MixBlend {
-                                            task_id: self.batcher.render_task_id,
+                                            task_id: self.batchers[0].render_task_id,
                                             source_id: cache_task_id,
                                             backdrop_id,
                                         },
                                     ),
                                     BlendMode::PremultipliedAlpha,
                                     BatchTextures::no_texture(),
                                 );
                                 let backdrop_task_address = render_tasks.get_task_address(backdrop_id);
@@ -1674,16 +1509,17 @@ impl BatchBuilder {
                                     bounding_rect,
                                     z_id,
                                     INVALID_SEGMENT_INDEX,
                                     EdgeAaSegmentMask::empty(),
                                     clip_task_address,
                                     brush_flags,
                                     prim_header_index,
                                     0,
+                                    prim_vis_mask,
                                 );
                             }
                             PictureCompositeMode::Blit(_) => {
                                 let cache_task_id = surface_task.expect("bug: surface must be allocated by now");
                                 let uv_rect_address = render_tasks[cache_task_id]
                                     .get_texture_address(gpu_cache)
                                     .as_int();
                                 let batch_params = BrushBatchParameters::shared(
@@ -1740,16 +1576,17 @@ impl BatchBuilder {
                                     specified_blend_mode,
                                     non_segmented_blend_mode,
                                     prim_header_index,
                                     bounding_rect,
                                     transform_kind,
                                     render_tasks,
                                     z_id,
                                     prim_info.clip_task_index,
+                                    prim_vis_mask,
                                     ctx,
                                 );
                             }
                         }
                     }
                     None => {
                         // If this picture is being drawn into an existing target (i.e. with
                         // no composition operation), recurse and add to the current batch list.
@@ -1828,16 +1665,17 @@ impl BatchBuilder {
                     specified_blend_mode,
                     non_segmented_blend_mode,
                     prim_header_index,
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_info.clip_task_index,
+                    prim_vis_mask,
                     ctx,
                 );
             }
             PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, opacity_binding_index, .. } => {
                 let prim_data = &ctx.data_stores.prim[data_handle];
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
                 let opacity_binding = ctx.prim_store.get_opacity_binding(opacity_binding_index);
 
@@ -1889,16 +1727,17 @@ impl BatchBuilder {
                     specified_blend_mode,
                     non_segmented_blend_mode,
                     prim_header_index,
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_info.clip_task_index,
+                    prim_vis_mask,
                     ctx,
                 );
             }
             PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, .. } => {
                 let yuv_image_data = &ctx.data_stores.yuv_image[data_handle].kind;
                 let mut textures = BatchTextures::no_texture();
                 let mut uv_rect_addresses = [0; 3];
 
@@ -1996,16 +1835,17 @@ impl BatchBuilder {
                     specified_blend_mode,
                     non_segmented_blend_mode,
                     prim_header_index,
                     bounding_rect,
                     transform_kind,
                     render_tasks,
                     z_id,
                     prim_info.clip_task_index,
+                    prim_vis_mask,
                     ctx,
                 );
             }
             PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
                 let image_data = &ctx.data_stores.image[data_handle].kind;
                 let common_data = &ctx.data_stores.image[data_handle].common;
                 let image_instance = &ctx.prim_store.images[image_instance_index];
                 let opacity_binding = ctx.prim_store.get_opacity_binding(image_instance.opacity_binding_index);
@@ -2100,16 +1940,17 @@ impl BatchBuilder {
                         specified_blend_mode,
                         non_segmented_blend_mode,
                         prim_header_index,
                         bounding_rect,
                         transform_kind,
                         render_tasks,
                         z_id,
                         prim_info.clip_task_index,
+                        prim_vis_mask,
                         ctx,
                     );
                 } else {
                     const VECS_PER_SPECIFIC_BRUSH: usize = 3;
                     let max_tiles_per_header = (MAX_VERTEX_TEXTURE_WIDTH - VECS_PER_SPECIFIC_BRUSH) / VECS_PER_SEGMENT;
 
                     let clip_task_address = ctx.get_prim_clip_task_address(
                         prim_info.clip_task_index,
@@ -2157,16 +1998,17 @@ impl BatchBuilder {
                                     bounding_rect,
                                     z_id,
                                     i as i32,
                                     tile.edge_flags,
                                     clip_task_address,
                                     BrushFlags::SEGMENT_RELATIVE | BrushFlags::PERSPECTIVE_INTERPOLATION,
                                     prim_header_index,
                                     uv_rect_address.as_int(),
+                                    prim_vis_mask,
                                 );
                             }
                         }
                     }
                 }
             }
             PrimitiveInstanceKind::LinearGradient { data_handle, gradient_index, .. } => {
                 let gradient = &ctx.prim_store.linear_gradients[gradient_index];
@@ -2233,16 +2075,17 @@ impl BatchBuilder {
                         bounding_rect,
                         z_id,
                         INVALID_SEGMENT_INDEX,
                         EdgeAaSegmentMask::all(),
                         clip_task_address,
                         BrushFlags::PERSPECTIVE_INTERPOLATION,
                         prim_header_index,
                         segment_user_data,
+                        prim_vis_mask,
                     );
                 } else if gradient.visible_tiles_range.is_empty() {
                     let batch_params = BrushBatchParameters::shared(
                         BrushBatchKind::LinearGradient,
                         BatchTextures::no_texture(),
                         [
                             prim_data.stops_handle.as_int(gpu_cache),
                             0,
@@ -2273,16 +2116,17 @@ impl BatchBuilder {
                         specified_blend_mode,
                         non_segmented_blend_mode,
                         prim_header_index,
                         bounding_rect,
                         transform_kind,
                         render_tasks,
                         z_id,
                         prim_info.clip_task_index,
+                        prim_vis_mask,
                         ctx,
                     );
                 } else {
                     let visible_tiles = &ctx.scratch.gradient_tiles[gradient.visible_tiles_range];
 
                     let clip_task_address = ctx.get_prim_clip_task_address(
                         prim_info.clip_task_index,
                         render_tasks,
@@ -2294,16 +2138,17 @@ impl BatchBuilder {
                         BrushBatchKind::LinearGradient,
                         specified_blend_mode,
                         bounding_rect,
                         clip_task_address,
                         gpu_cache,
                         &prim_header,
                         prim_headers,
                         z_id,
+                        prim_vis_mask,
                     );
                 }
             }
             PrimitiveInstanceKind::RadialGradient { data_handle, ref visible_tiles_range, .. } => {
                 let prim_data = &ctx.data_stores.radial_grad[data_handle];
                 let specified_blend_mode = BlendMode::PremultipliedAlpha;
 
                 let mut prim_header = PrimitiveHeader {
@@ -2357,16 +2202,17 @@ impl BatchBuilder {
                         specified_blend_mode,
                         non_segmented_blend_mode,
                         prim_header_index,
                         bounding_rect,
                         transform_kind,
                         render_tasks,
                         z_id,
                         prim_info.clip_task_index,
+                        prim_vis_mask,
                         ctx,
                     );
                 } else {
                     let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
 
                     let clip_task_address = ctx.get_prim_clip_task_address(
                         prim_info.clip_task_index,
                         render_tasks,
@@ -2378,16 +2224,17 @@ impl BatchBuilder {
                         BrushBatchKind::RadialGradient,
                         specified_blend_mode,
                         bounding_rect,
                         clip_task_address,
                         gpu_cache,
                         &prim_header,
                         prim_headers,
                         z_id,
+                        prim_vis_mask,
                     );
                 }
             }
         }
     }
 
     /// Add a single segment instance to a batch.
     fn add_segment_to_batch(
@@ -2399,16 +2246,17 @@ impl BatchBuilder {
         prim_header_index: PrimitiveHeaderIndex,
         alpha_blend_mode: BlendMode,
         bounding_rect: &PictureRect,
         transform_kind: TransformedRectKind,
         render_tasks: &RenderTaskGraph,
         z_id: ZBufferId,
         prim_opacity: PrimitiveOpacity,
         clip_task_index: ClipTaskIndex,
+        prim_vis_mask: PrimitiveVisibilityMask,
         ctx: &RenderTargetContext,
     ) {
         debug_assert!(clip_task_index != ClipTaskIndex::INVALID);
 
         // Get GPU address of clip task for this segment, or None if
         // the entire segment is clipped out.
         let clip_task_address = match ctx.get_clip_task_address(
             clip_task_index,
@@ -2436,16 +2284,17 @@ impl BatchBuilder {
             bounding_rect,
             z_id,
             segment_index,
             segment.edge_flags,
             clip_task_address,
             BrushFlags::PERSPECTIVE_INTERPOLATION | segment.brush_flags,
             prim_header_index,
             segment_data.user_data,
+            prim_vis_mask,
         );
     }
 
     /// Add any segment(s) from a brush to batches.
     fn add_segmented_prim_to_batch(
         &mut self,
         brush_segments: Option<&[BrushSegment]>,
         prim_opacity: PrimitiveOpacity,
@@ -2453,16 +2302,17 @@ impl BatchBuilder {
         alpha_blend_mode: BlendMode,
         non_segmented_blend_mode: BlendMode,
         prim_header_index: PrimitiveHeaderIndex,
         bounding_rect: &PictureRect,
         transform_kind: TransformedRectKind,
         render_tasks: &RenderTaskGraph,
         z_id: ZBufferId,
         clip_task_index: ClipTaskIndex,
+        prim_vis_mask: PrimitiveVisibilityMask,
         ctx: &RenderTargetContext,
     ) {
         match (brush_segments, &params.segment_data) {
             (Some(ref brush_segments), SegmentDataKind::Instanced(ref segment_data)) => {
                 // In this case, we have both a list of segments, and a list of
                 // per-segment instance data. Zip them together to build batches.
                 debug_assert_eq!(brush_segments.len(), segment_data.len());
                 for (segment_index, (segment, segment_data)) in brush_segments
@@ -2478,16 +2328,17 @@ impl BatchBuilder {
                         prim_header_index,
                         alpha_blend_mode,
                         bounding_rect,
                         transform_kind,
                         render_tasks,
                         z_id,
                         prim_opacity,
                         clip_task_index,
+                        prim_vis_mask,
                         ctx,
                     );
                 }
             }
             (Some(ref brush_segments), SegmentDataKind::Shared(ref segment_data)) => {
                 // A list of segments, but the per-segment data is common
                 // between all segments.
                 for (segment_index, segment) in brush_segments
@@ -2502,16 +2353,17 @@ impl BatchBuilder {
                         prim_header_index,
                         alpha_blend_mode,
                         bounding_rect,
                         transform_kind,
                         render_tasks,
                         z_id,
                         prim_opacity,
                         clip_task_index,
+                        prim_vis_mask,
                         ctx,
                     );
                 }
             }
             (None, SegmentDataKind::Shared(ref segment_data)) => {
                 // No segments, and thus no per-segment instance data.
                 // Note: the blend mode already takes opacity into account
                 let batch_key = BatchKey {
@@ -2528,16 +2380,17 @@ impl BatchBuilder {
                     bounding_rect,
                     z_id,
                     INVALID_SEGMENT_INDEX,
                     EdgeAaSegmentMask::all(),
                     clip_task_address,
                     BrushFlags::PERSPECTIVE_INTERPOLATION,
                     prim_header_index,
                     segment_data.user_data,
+                    prim_vis_mask,
                 );
             }
             (None, SegmentDataKind::Instanced(..)) => {
                 // We should never hit the case where there are no segments,
                 // but a list of segment instance data.
                 unreachable!();
             }
         }
@@ -2550,16 +2403,17 @@ impl BatchBuilder {
         kind: BrushBatchKind,
         blend_mode: BlendMode,
         bounding_rect: &PictureRect,
         clip_task_address: RenderTaskAddress,
         gpu_cache: &GpuCache,
         base_prim_header: &PrimitiveHeader,
         prim_headers: &mut PrimitiveHeaders,
         z_id: ZBufferId,
+        prim_vis_mask: PrimitiveVisibilityMask,
     ) {
         let key = BatchKey {
             blend_mode,
             kind: BatchKind::Brush(kind),
             textures: BatchTextures::no_texture(),
         };
 
         let user_data = [stops_handle.as_int(gpu_cache), 0, 0, 0];
@@ -2586,16 +2440,17 @@ impl BatchBuilder {
                 bounding_rect,
                 z_id,
                 INVALID_SEGMENT_INDEX,
                 EdgeAaSegmentMask::all(),
                 clip_task_address,
                 BrushFlags::PERSPECTIVE_INTERPOLATION,
                 prim_header_index,
                 0,
+                prim_vis_mask,
             );
         }
     }
 }
 
 fn get_image_tile_params(
     resource_cache: &ResourceCache,
     gpu_cache: &mut GpuCache,
@@ -2692,16 +2547,18 @@ impl PrimitiveInstance {
             PrimitiveInstanceKind::Picture { .. } |
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::LineDecoration { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } |
             PrimitiveInstanceKind::Rectangle { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain |
             PrimitiveInstanceKind::Clear { .. } => {
                 return true;
             }
         };
         match resource_cache.get_image_properties(image_key) {
             Some(ImageProperties { external_image: Some(_), .. }) => {
                 false
             }
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -10,20 +10,21 @@ use crate::box_shadow::{BLUR_SAMPLE_SCAL
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, CoordinateSystemId, ClipScrollTree, SpatialNodeIndex};
 use crate::ellipse::Ellipse;
 use crate::gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use crate::gpu_types::{BoxShadowStretchMode};
 use crate::image::{self, Repetition};
 use crate::intern;
 use crate::prim_store::{ClipData, ImageMaskData, SpaceMapper, VisibleMaskImageTile};
 use crate::prim_store::{PointKey, SizeKey, RectangleKey};
+use crate::render_backend::DataStores;
 use crate::render_task::to_cache_size;
 use crate::resource_cache::{ImageRequest, ResourceCache};
 use std::{cmp, ops, u32};
-use crate::util::{extract_inner_rect_safe, project_rect, ScaleOffset};
+use crate::util::{extract_inner_rect_safe, project_rect, ScaleOffset, MaxRect};
 
 /*
 
  Module Overview
 
  There are a number of data structures involved in the clip module:
 
  ClipStore - Main interface used by other modules.
@@ -236,17 +237,17 @@ pub struct ClipNodeInstance {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeRange {
     pub first: u32,
     pub count: u32,
 }
 
 impl ClipNodeRange {
-    fn to_range(&self) -> ops::Range<usize> {
+    pub fn to_range(&self) -> ops::Range<usize> {
         let start = self.first as usize;
         let end = start + self.count as usize;
 
         ops::Range {
             start,
             end,
         }
     }
@@ -502,17 +503,17 @@ impl ClipNode {
     }
 }
 
 /// The main clipping public interface that other modules access.
 #[derive(MallocSizeOf)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct ClipStore {
     pub clip_chain_nodes: Vec<ClipChainNode>,
-    clip_node_instances: Vec<ClipNodeInstance>,
+    pub clip_node_instances: Vec<ClipNodeInstance>,
 
     active_clip_node_info: Vec<ClipNodeInfo>,
     active_local_clip_rect: Option<LayoutRect>,
 }
 
 // A clip chain instance is what gets built for a given clip
 // chain id + local primitive region + positioning node.
 #[derive(Debug)]
@@ -543,59 +544,162 @@ impl ClipChainInstance {
             has_non_local_clips: false,
             needs_mask: false,
             pic_clip_rect: PictureRect::zero(),
             pic_spatial_node_index: ROOT_SPATIAL_NODE_INDEX,
         }
     }
 }
 
+/// Maintains a (flattened) list of clips for a given level in the surface level stack.
+pub struct ClipChainLevel {
+    clips: Vec<ClipChainId>,
+    clip_counts: Vec<usize>,
+    viewport: WorldRect,
+}
+
+impl ClipChainLevel {
+    /// Construct a new level in the active clip chain stack. The viewport
+    /// is used to filter out irrelevant clips.
+    fn new(
+        viewport: WorldRect,
+    ) -> Self {
+        ClipChainLevel {
+            clips: Vec::new(),
+            clip_counts: Vec::new(),
+            viewport,
+        }
+    }
+}
+
 /// Maintains a stack of clip chain ids that are currently active,
 /// when a clip exists on a picture that has no surface, and is passed
 /// on down to the child primitive(s).
 pub struct ClipChainStack {
     // TODO(gw): Consider using SmallVec, or recycling the clip stacks here.
     /// A stack of clip chain lists. Each time a new surface is pushed,
     /// a new entry is added to the main stack. Each time a new picture
     /// without surface is pushed, it adds the picture clip chain to the
     /// current stack list.
-    pub stack: Vec<Vec<ClipChainId>>,
+    pub stack: Vec<ClipChainLevel>,
 }
 
 impl ClipChainStack {
     pub fn new() -> Self {
         ClipChainStack {
-            stack: vec![vec![]],
+            stack: vec![ClipChainLevel::new(WorldRect::max_rect())],
         }
     }
 
     /// Push a clip chain root onto the currently active list.
-    pub fn push_clip(&mut self, clip_chain_id: ClipChainId) {
-        self.stack.last_mut().unwrap().push(clip_chain_id);
+    pub fn push_clip(
+        &mut self,
+        clip_chain_id: ClipChainId,
+        clip_store: &ClipStore,
+        data_stores: &DataStores,
+        clip_scroll_tree: &ClipScrollTree,
+        global_screen_world_rect: WorldRect,
+    ) {
+        let level = self.stack.last_mut().unwrap();
+        let mut clip_count = 0;
+
+        let mut current_clip_chain_id = clip_chain_id;
+        while current_clip_chain_id != ClipChainId::NONE {
+            let clip_chain_node = &clip_store.clip_chain_nodes[current_clip_chain_id.0 as usize];
+
+            let clip_node = &data_stores.clip[clip_chain_node.handle];
+
+            // Filter out irrelevant rectangle clips. If a clip rect is in world space
+            // and completely contains the viewport to be rendered, then the clip itself
+            // is redundant. This is particularly important for picture caching, to avoid
+            // extra invalidations. By skipping these global clip rects (such as the iframe rect)
+            // we can (a) avoid invalidations due to introducing clip dependencies where the
+            // relative transform changes during scrolling, and (b) allow correct rendering
+            // of tiles that exceed these clip rects, so that we can draw them once and
+            // cache them, to be used during scrolling.
+            let valid_clip = match clip_node.item {
+                ClipItem::Rectangle(size, ClipMode::Clip) => {
+                    let scroll_root = clip_scroll_tree.find_scroll_root(
+                        clip_chain_node.spatial_node_index,
+                    );
+
+                    let local_clip_rect = LayoutRect::new(
+                        clip_chain_node.local_pos,
+                        size,
+                    );
+
+                    let mut is_required = true;
+
+                    if scroll_root == ROOT_SPATIAL_NODE_INDEX {
+                        let map_local_to_world = SpaceMapper::new_with_target(
+                            ROOT_SPATIAL_NODE_INDEX,
+                            clip_chain_node.spatial_node_index,
+                            global_screen_world_rect,
+                            clip_scroll_tree,
+                        );
+
+                        // TODO(gw): This map method can produce a conservative bounding rect
+                        //           which is not what we require here. In this case, we know
+                        //           that the conversion is exect, due to checking that the
+                        //           scroll root is the root spatial node. However, we should
+                        //           change this to directly use the content_transform, to make
+                        //           the intent clearer here.
+                        if let Some(clip_world_rect) = map_local_to_world.map(&local_clip_rect) {
+                            if clip_world_rect.contains_rect(&level.viewport) {
+                                is_required = false;
+                            }
+                        }
+                    }
+
+                    is_required
+                }
+                _ => {
+                    true
+                }
+            };
+
+            if valid_clip {
+                level.clips.push(current_clip_chain_id);
+                clip_count += 1;
+            }
+
+            current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+        }
+
+        level.clip_counts.push(clip_count);
     }
 
     /// Pop a clip chain root from the currently active list.
     pub fn pop_clip(&mut self) {
-        self.stack.last_mut().unwrap().pop().unwrap();
+        let level = self.stack.last_mut().unwrap();
+        let count = level.clip_counts.pop().unwrap();
+        for _ in 0 .. count {
+            level.clips.pop().unwrap();
+        }
     }
 
     /// When a surface is created, it takes all clips and establishes a new
     /// stack of clips to be propagated.
-    pub fn push_surface(&mut self) {
-        self.stack.push(Vec::new());
+    pub fn push_surface(
+        &mut self,
+        viewport: WorldRect,
+    ) {
+        let level = ClipChainLevel::new(viewport);
+        self.stack.push(level);
     }
 
     /// Pop a surface from the clip chain stack
     pub fn pop_surface(&mut self) {
-        self.stack.pop().unwrap();
+        let level = self.stack.pop().unwrap();
+        assert!(level.clip_counts.is_empty() && level.clips.is_empty());
     }
 
     /// Get the list of currently active clip chains
-    pub fn current_clips(&self) -> &[ClipChainId] {
-        self.stack.last().unwrap()
+    pub fn current_clips_array(&self) -> &[ClipChainId] {
+        &self.stack.last().unwrap().clips
     }
 }
 
 impl ClipStore {
     pub fn new() -> Self {
         ClipStore {
             clip_chain_nodes: Vec::new(),
             clip_node_instances: Vec::new(),
@@ -645,35 +749,28 @@ impl ClipStore {
         clip_scroll_tree: &ClipScrollTree,
         clip_data_store: &mut ClipDataStore,
     ) {
         self.active_clip_node_info.clear();
         self.active_local_clip_rect = None;
 
         let mut local_clip_rect = local_prim_clip_rect;
 
-        for clip_chain_root in clip_chains {
-            let mut current_clip_chain_id = *clip_chain_root;
-
-            // for each clip chain node
-            while current_clip_chain_id != ClipChainId::NONE {
-                let clip_chain_node = &self.clip_chain_nodes[current_clip_chain_id.0 as usize];
+        for clip_chain_id in clip_chains {
+            let clip_chain_node = &self.clip_chain_nodes[clip_chain_id.0 as usize];
 
-                if !add_clip_node_to_current_chain(
-                    clip_chain_node,
-                    spatial_node_index,
-                    &mut local_clip_rect,
-                    &mut self.active_clip_node_info,
-                    clip_data_store,
-                    clip_scroll_tree,
-                ) {
-                    return;
-                }
-
-                current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+            if !add_clip_node_to_current_chain(
+                clip_chain_node,
+                spatial_node_index,
+                &mut local_clip_rect,
+                &mut self.active_clip_node_info,
+                clip_data_store,
+                clip_scroll_tree,
+            ) {
+                return;
             }
         }
 
         self.active_local_clip_rect = Some(local_clip_rect);
     }
 
     /// Setup the active clip chains, based on an existing primitive clip chain instance.
     pub fn set_active_clips_from_clip_chain(
--- a/gfx/wr/webrender/src/clip_scroll_tree.rs
+++ b/gfx/wr/webrender/src/clip_scroll_tree.rs
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::units::*;
-use euclid::{TypedPoint2D, TypedScale, TypedTransform3D};
+use euclid::TypedTransform3D;
 use crate::gpu_types::TransformPalette;
 use crate::internal_types::{FastHashMap, FastHashSet};
 use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use crate::scene::SceneProperties;
 use crate::spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
 use std::{ops, u32};
 use crate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
 
@@ -179,40 +179,16 @@ impl<Src, Dst> CoordinateSpaceMapping<Sr
     pub fn is_perspective(&self) -> bool {
         match *self {
             CoordinateSpaceMapping::Local |
             CoordinateSpaceMapping::ScaleOffset(_) => false,
             CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
         }
     }
 
-    pub fn project_2d_origin(&self) -> Option<TypedPoint2D<f32, Dst>> {
-        match *self {
-            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
-            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
-                scale_offset.offset.to_point() * TypedScale::new(1.0)
-            ),
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                transform.transform_point2d(&TypedPoint2D::zero())
-            }
-        }
-    }
-
-    pub fn inverse_project_2d_origin(&self) -> Option<TypedPoint2D<f32, Src>> {
-        match *self {
-            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
-            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
-                scale_offset.inverse().offset.to_point() * TypedScale::new(1.0)
-            ),
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                transform.inverse_project_2d_origin()
-            }
-        }
-    }
-
     pub fn scale_factors(&self) -> (f32, f32) {
         match *self {
             CoordinateSpaceMapping::Local => (1.0, 1.0),
             CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x, scale_offset.scale.y),
             CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
         }
     }
 
@@ -362,38 +338,16 @@ impl ClipScrollTree {
     /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset.
     pub fn get_world_viewport_transform(
         &self,
         index: SpatialNodeIndex,
     ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
         self.get_world_transform_impl(index, TransformScroll::Unscrolled)
     }
 
-
-    /// 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;
-
-        loop {
-            if index == parent_spatial_node_index {
-                return true;
-            }
-
-            index = match self.spatial_nodes[index.0 as usize].parent {
-                Some(parent) => parent,
-                None => return false,
-            }
-        }
-    }
-
     /// The root reference frame, which is the true root of the ClipScrollTree. Initially
     /// this ID is not valid, which is indicated by ```spatial_nodes``` being empty.
     pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
         // TODO(mrobinson): We should eventually make this impossible to misuse.
         debug_assert!(!self.spatial_nodes.is_empty());
         ROOT_SPATIAL_NODE_INDEX
     }
 
@@ -627,16 +581,47 @@ impl ClipScrollTree {
         self.spatial_nodes.push(node);
         index
     }
 
     pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
         self.pipelines_to_discard.insert(pipeline_id);
     }
 
+    /// Find the spatial node that is the scroll root for a given spatial node.
+    /// A scroll root is the first spatial node when found travelling up the
+    /// spatial node tree that is an explicit scroll frame.
+    pub fn find_scroll_root(
+        &self,
+        spatial_node_index: SpatialNodeIndex,
+    ) -> SpatialNodeIndex {
+        let mut scroll_root = ROOT_SPATIAL_NODE_INDEX;
+        let mut node_index = spatial_node_index;
+
+        while node_index != ROOT_SPATIAL_NODE_INDEX {
+            let node = &self.spatial_nodes[node_index.0 as usize];
+            match node.node_type {
+                SpatialNodeType::ReferenceFrame(..) |
+                SpatialNodeType::StickyFrame(..) => {
+                    // TODO(gw): In future, we may need to consider sticky frames.
+                }
+                SpatialNodeType::ScrollFrame(ref info) => {
+                    // If we found an explicit scroll root, store that
+                    // and keep looking up the tree.
+                    if let ScrollFrameKind::Explicit = info.frame_kind {
+                        scroll_root = node_index;
+                    }
+                }
+            }
+            node_index = node.parent.expect("unable to find parent node");
+        }
+
+        scroll_root
+    }
+
     fn print_node<T: PrintTreePrinter>(
         &self,
         index: SpatialNodeIndex,
         pt: &mut T,
     ) {
         let node = &self.spatial_nodes[index.0 as usize];
         match node.node_type {
             SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
@@ -644,16 +629,17 @@ impl ClipScrollTree {
                 pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
             }
             SpatialNodeType::ScrollFrame(scrolling_info) => {
                 pt.new_level(format!("ScrollFrame"));
                 pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
                 pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
                 pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
                 pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
+                pt.add_item(format!("kind: {:?}", scrolling_info.frame_kind));
             }
             SpatialNodeType::ReferenceFrame(ref info) => {
                 pt.new_level(format!("ReferenceFrame"));
                 pt.add_item(format!("kind: {:?}", info.kind));
                 pt.add_item(format!("transform_style: {:?}", info.transform_style));
             }
         }
 
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -16,33 +16,33 @@ use crate::clip::{ClipChainId, ClipRegio
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use crate::frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use crate::glyph_rasterizer::FontInstance;
 use crate::hit_test::{HitTestingItem, HitTestingScene};
 use crate::image::simplify_repeated_primitive;
 use crate::intern::Interner;
 use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo, Filter};
 use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
-use crate::picture::{BlitReason, PrimitiveList, TileCache};
+use crate::picture::{BlitReason, PrimitiveList, TileCacheInstance};
 use crate::prim_store::{PrimitiveInstance, PrimitiveSceneData};
 use crate::prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
 use crate::prim_store::{ScrollNodeAndClipChain, PictureIndex};
 use crate::prim_store::{InternablePrimitive, SegmentInstanceIndex};
 use crate::prim_store::{register_prim_chase_id, get_line_decoration_sizes};
 use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
 use crate::prim_store::gradient::{GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams};
 use crate::prim_store::image::{Image, YuvImage};
 use crate::prim_store::line_dec::{LineDecoration, LineDecorationCacheKey};
 use crate::prim_store::picture::{Picture, PictureCompositeKey, PictureKey};
 use crate::prim_store::text_run::TextRun;
 use crate::render_backend::{DocumentView};
 use crate::resource_cache::{FontInstanceMap, ImageRequest};
 use crate::scene::{Scene, StackingContextHelpers};
 use crate::scene_builder::{DocumentStats, Interners};
-use crate::spatial_node::{StickyFrameInfo, ScrollFrameKind, SpatialNodeType};
+use crate::spatial_node::{StickyFrameInfo, ScrollFrameKind};
 use std::{f32, mem, usize, ops};
 use std::collections::vec_deque::VecDeque;
 use std::sync::Arc;
 use crate::tiling::{CompositeOps};
 use crate::util::{MaxRect, VecHelper};
 use crate::filterdata::{SFilterDataComponent, SFilterData, SFilterDataKey};
 
 #[derive(Debug, Copy, Clone)]
@@ -254,16 +254,20 @@ pub struct DisplayListFlattener<'a> {
     /// to start the culling phase from.
     pub root_pic_index: PictureIndex,
 
     /// Helper struct to map stacking context coords <-> reference frame coords.
     rf_mapper: ReferenceFrameMapper,
 
     /// Helper struct to map spatial nodes to external scroll offsets.
     external_scroll_mapper: ScrollOffsetMapper,
+
+    /// If true, a stacking context with create_tile_cache set to true was found
+    /// during flattening.
+    found_explicit_tile_cache: bool,
 }
 
 impl<'a> DisplayListFlattener<'a> {
     pub fn create_frame_builder(
         scene: &Scene,
         clip_scroll_tree: &mut ClipScrollTree,
         font_instances: FontInstanceMap,
         view: &DocumentView,
@@ -293,16 +297,17 @@ impl<'a> DisplayListFlattener<'a> {
             sc_stack: Vec::new(),
             pipeline_clip_chain_stack: vec![ClipChainId::NONE],
             prim_store: PrimitiveStore::new(&doc_stats.prim_store_stats),
             clip_store: ClipStore::new(),
             interners,
             root_pic_index: PictureIndex(0),
             rf_mapper: ReferenceFrameMapper::new(),
             external_scroll_mapper: ScrollOffsetMapper::new(),
+            found_explicit_tile_cache: false,
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
 
@@ -366,21 +371,16 @@ impl<'a> DisplayListFlattener<'a> {
             .external_scroll_offset(
                 spatial_node_index,
                 &self.clip_scroll_tree,
             );
 
         rf_offset + scroll_offset
     }
 
-    /// Cut the primitives in the root stacking context based on the picture
-    /// caching scroll root. This is a temporary solution for the initial
-    /// implementation of picture caching. We need to work out the specifics
-    /// of how WR should decide (or Gecko should communicate) where the main
-    /// content frame is that should get surface caching.
     fn setup_picture_caching(
         &mut self,
         primitives: &mut Vec<PrimitiveInstance>,
     ) {
         if !self.config.enable_picture_caching {
             return;
         }
 
@@ -409,17 +409,17 @@ impl<'a> DisplayListFlattener<'a> {
         // most of the content is embedded in its own picture.
         //
 
         // Find the first primitive which has the desired scroll root.
         let mut first_index = None;
         let mut main_scroll_root = None;
 
         for (i, instance) in primitives.iter().enumerate() {
-            let scroll_root = self.find_scroll_root(
+            let scroll_root = self.clip_scroll_tree.find_scroll_root(
                 instance.spatial_node_index,
             );
 
             if scroll_root != ROOT_SPATIAL_NODE_INDEX {
                 // If we find multiple scroll roots in this page, then skip
                 // picture caching for now. In future, we can handle picture
                 // caching on these sites by creating a tile cache per
                 // scroll root, or (more likely) selecting the common parent
@@ -460,17 +460,17 @@ impl<'a> DisplayListFlattener<'a> {
 
         match first_index {
             Some(first_index) => {
                 // Split off the preceding primtives.
                 remaining_prims = old_prim_list.split_off(first_index);
 
                 // Find the first primitive in reverse order that is not the root scroll node.
                 let last_index = remaining_prims.iter().rposition(|instance| {
-                    let scroll_root = self.find_scroll_root(
+                    let scroll_root = self.clip_scroll_tree.find_scroll_root(
                         instance.spatial_node_index,
                     );
 
                     scroll_root != ROOT_SPATIAL_NODE_INDEX
                 }).unwrap_or(remaining_prims.len() - 1);
 
                 preceding_prims = old_prim_list;
                 trailing_prims = remaining_prims.split_off(last_index + 1);
@@ -500,29 +500,27 @@ impl<'a> DisplayListFlattener<'a> {
         let pic_data_handle = self.interners
             .picture
             .intern(&pic_key, || {
                 PrimitiveSceneData {
                     prim_size: LayoutSize::zero(),
                     is_backface_visible: true,
                 }
             }
-        );
-
-        let tile_cache = Box::new(TileCache::new(
+            );
+
+        let tile_cache = Box::new(TileCacheInstance::new(
+            0,
             main_scroll_root,
-            &prim_list.prim_instances,
-            *self.pipeline_clip_chain_stack.last().unwrap(),
-            &self.prim_store.pictures,
+            self.config.background_color,
         ));
 
         let pic_index = self.prim_store.pictures.alloc().init(PicturePrimitive::new_image(
-            Some(PictureCompositeMode::TileCache { clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0) }),
+            Some(PictureCompositeMode::TileCache { }),
             Picture3DContext::Out,
-            self.scene.root_pipeline_id.unwrap(),
             None,
             true,
             true,
             RasterSpace::Screen,
             prim_list,
             main_scroll_root,
             Some(tile_cache),
             PictureOptions::default(),
@@ -543,46 +541,16 @@ impl<'a> DisplayListFlattener<'a> {
         // This contains the tile caching picture, with preceding and
         // trailing primitives outside the main scroll root.
         primitives.reserve(preceding_prims.len() + trailing_prims.len() + 1);
         primitives.extend(preceding_prims);
         primitives.push(instance);
         primitives.extend(trailing_prims);
     }
 
-    /// Find the spatial node that is the scroll root for a given
-    /// spatial node.
-    fn find_scroll_root(
-        &self,
-        spatial_node_index: SpatialNodeIndex,
-    ) -> SpatialNodeIndex {
-        let mut scroll_root = ROOT_SPATIAL_NODE_INDEX;
-        let mut node_index = spatial_node_index;
-
-        while node_index != ROOT_SPATIAL_NODE_INDEX {
-            let node = &self.clip_scroll_tree.spatial_nodes[node_index.0 as usize];
-            match node.node_type {
-                SpatialNodeType::ReferenceFrame(..) |
-                SpatialNodeType::StickyFrame(..) => {
-                    // TODO(gw): In future, we may need to consider sticky frames.
-                }
-                SpatialNodeType::ScrollFrame(ref info) => {
-                    // If we found an explicit scroll root, store that
-                    // and keep looking up the tree.
-                    if let ScrollFrameKind::Explicit = info.frame_kind {
-                        scroll_root = node_index;
-                    }
-                }
-            }
-            node_index = node.parent.expect("unable to find parent node");
-        }
-
-        scroll_root
-    }
-
     fn flatten_items(
         &mut self,
         traversal: &mut BuiltDisplayListIter<'a>,
         pipeline_id: PipelineId,
         apply_pipeline_clip: bool,
     ) {
         loop {
             let subtraversal = {
@@ -1390,16 +1358,17 @@ impl<'a> DisplayListFlattener<'a> {
     pub fn add_primitive_to_draw_list(
         &mut self,
         prim_instance: PrimitiveInstance,
     ) {
         // Add primitive to the top-most stacking context on the stack.
         if prim_instance.is_chased() {
             println!("\tadded to stacking context at {}", self.sc_stack.len());
         }
+
         let stacking_context = self.sc_stack.last_mut().unwrap();
         stacking_context.primitives.push(prim_instance);
     }
 
     /// Convenience interface that creates a primitive entry and adds it
     /// to the draw list.
     fn add_nonshadowable_primitive<P>(
         &mut self,
@@ -1502,16 +1471,19 @@ 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
         };
 
+        // Mark if a user supplied tile cache was specified.
+        self.found_explicit_tile_cache |= create_tile_cache;
+
         if is_pipeline_root && create_tile_cache && self.config.enable_picture_caching {
             // we don't expect any nested tile-cache-enabled stacking contexts
             debug_assert!(!self.sc_stack.iter().any(|sc| sc.create_tile_cache));
         }
 
         // Get the transform-style of the parent stacking context,
         // which determines if we *might* need to draw this on
         // an intermediate surface for plane splitting purposes.
@@ -1612,24 +1584,43 @@ impl<'a> DisplayListFlattener<'a> {
         //     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) {
+                    if stacking_context.clip_chain_id != ClipChainId::NONE {
+                        let prim = create_clip_prim_instance(
+                            stacking_context.spatial_node_index,
+                            stacking_context.clip_chain_id,
+                            PrimitiveInstanceKind::PushClipChain,
+                        );
+                        parent_sc.primitives.push(prim);
+                    }
+
                     // 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);
                     }
+
+                    if stacking_context.clip_chain_id != ClipChainId::NONE {
+                        let prim = create_clip_prim_instance(
+                            stacking_context.spatial_node_index,
+                            stacking_context.clip_chain_id,
+                            PrimitiveInstanceKind::PopClipChain,
+                        );
+                        parent_sc.primitives.push(prim);
+                    }
+
                     return;
                 }
                 parent_sc.primitives.is_empty()
             },
             None => true,
         };
 
         if stacking_context.create_tile_cache {
@@ -1660,23 +1651,91 @@ impl<'a> DisplayListFlattener<'a> {
                 } 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
             ),
         };
 
+        // If no user supplied tile cache was specified, and picture caching is enabled,
+        // create an implicit tile cache for the whole frame builder.
+        // TODO(gw): This is only needed temporarily - once we support multiple slices
+        //           correctly, this will be handled by setup_picture_caching.
+        if self.sc_stack.is_empty() &&
+            !self.found_explicit_tile_cache &&
+            self.config.enable_picture_caching {
+
+            let scroll_root = ROOT_SPATIAL_NODE_INDEX;
+
+            let prim_list = PrimitiveList::new(
+                stacking_context.primitives,
+                &self.interners,
+            );
+
+            // Now, create a picture with tile caching enabled that will hold all
+            // of the primitives selected as belonging to the main scroll root.
+            let pic_key = PictureKey::new(
+                true,
+                LayoutSize::zero(),
+                Picture {
+                    composite_mode_key: PictureCompositeKey::Identity,
+                },
+            );
+
+            let pic_data_handle = self.interners
+                .picture
+                .intern(&pic_key, || {
+                    PrimitiveSceneData {
+                        prim_size: LayoutSize::zero(),
+                        is_backface_visible: true,
+                    }
+                }
+                );
+
+            let tile_cache = TileCacheInstance::new(
+                0,
+                ROOT_SPATIAL_NODE_INDEX,
+                self.config.background_color,
+            );
+
+            let pic_index = self.prim_store.pictures.alloc().init(PicturePrimitive::new_image(
+                Some(PictureCompositeMode::TileCache {}),
+                Picture3DContext::Out,
+                None,
+                true,
+                true,
+                RasterSpace::Screen,
+                prim_list,
+                scroll_root,
+                Some(Box::new(tile_cache)),
+                PictureOptions::default(),
+            ));
+
+            let instance = PrimitiveInstance::new(
+                LayoutPoint::zero(),
+                LayoutRect::max_rect(),
+                PrimitiveInstanceKind::Picture {
+                    data_handle: pic_data_handle,
+                    pic_index: PictureIndex(pic_index),
+                    segment_instance_index: SegmentInstanceIndex::INVALID,
+                },
+                ClipChainId::NONE,
+                scroll_root,
+            );
+
+            stacking_context.primitives = vec![instance];
+        }
+
         // 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.clone(),
                 leaf_context_3d,
-                stacking_context.pipeline_id,
                 leaf_output_pipeline_id,
                 true,
                 stacking_context.is_backface_visible,
                 stacking_context.requested_raster_space,
                 PrimitiveList::new(
                     stacking_context.primitives,
                     &self.interners,
                 ),
@@ -1713,17 +1772,16 @@ impl<'a> DisplayListFlattener<'a> {
             current_pic_index = PictureIndex(self.prim_store.pictures
                 .alloc()
                 .init(PicturePrimitive::new_image(
                     None,
                     Picture3DContext::In {
                         root_data: Some(Vec::new()),
                         ancestor_index,
                     },
-                    stacking_context.pipeline_id,
                     stacking_context.frame_output_pipeline_id,
                     true,
                     stacking_context.is_backface_visible,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         prims,
                         &self.interners,
                     ),
@@ -1780,17 +1838,16 @@ impl<'a> DisplayListFlattener<'a> {
                 _ => PictureCompositeMode::Filter(filter.clone()),
             });
 
             let filter_pic_index = PictureIndex(self.prim_store.pictures
                 .alloc()
                 .init(PicturePrimitive::new_image(
                     composite_mode.clone(),
                     Picture3DContext::Out,
-                    stacking_context.pipeline_id,
                     None,
                     true,
                     stacking_context.is_backface_visible,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
                         &self.interners,
                     ),
@@ -1834,17 +1891,16 @@ impl<'a> DisplayListFlattener<'a> {
         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));
 
             let blend_pic_index = PictureIndex(self.prim_store.pictures
                 .alloc()
                 .init(PicturePrimitive::new_image(
                     composite_mode.clone(),
                     Picture3DContext::Out,
-                    stacking_context.pipeline_id,
                     None,
                     true,
                     stacking_context.is_backface_visible,
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
                         &self.interners,
                     ),
@@ -2101,17 +2157,16 @@ impl<'a> DisplayListFlattener<'a> {
         }));
     }
 
     pub fn pop_all_shadows(
         &mut self,
     ) {
         assert!(!self.pending_shadow_items.is_empty(), "popped shadows, but none were present");
 
-        let pipeline_id = self.sc_stack.last().unwrap().pipeline_id;
         let mut items = mem::replace(&mut self.pending_shadow_items, VecDeque::new());
 
         //
         // The pending_shadow_items queue contains a list of shadows and primitives
         // that were pushed during the active shadow context. To process these, we:
         //
         // Iterate the list, popping an item from the front each iteration.
         //
@@ -2209,17 +2264,16 @@ impl<'a> DisplayListFlattener<'a> {
                         };
 
                         // Create the primitive to draw the shadow picture into the scene.
                         let shadow_pic_index = PictureIndex(self.prim_store.pictures
                             .alloc()
                             .init(PicturePrimitive::new_image(
                                 Some(composite_mode),
                                 Picture3DContext::Out,
-                                pipeline_id,
                                 None,
                                 is_passthrough,
                                 is_backface_visible,
                                 raster_space,
                                 PrimitiveList::new(
                                     prims,
                                     &self.interners,
                                 ),
@@ -2906,35 +2960,30 @@ 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() {
+            !parent.primitives.is_empty() {
             return false;
         }
 
-        // If backface visibility is different
-        if self.is_backface_visible != parent.is_backface_visible {
+        // If backface visibility is explicitly set.
+        if !self.is_backface_visible {
             return false;
         }
 
         // If rasterization space is different
         if self.requested_raster_space != parent.requested_raster_space {
             return false;
         }
 
-        // If different clip chains
-        if self.clip_chain_id != parent.clip_chain_id {
-            return false;
-        }
-
         // If need to isolate in surface due to clipping / mix-blend-mode
         if !self.blit_reason.is_empty() {
             return false;
         }
 
         // If this stacking context gets picture caching, we need it.
         if self.create_tile_cache {
             return false;
@@ -2962,17 +3011,16 @@ impl FlattenedStackingContext {
             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,
-                self.pipeline_id,
                 None,
                 true,
                 self.is_backface_visible,
                 self.requested_raster_space,
                 PrimitiveList::new(
                     mem::replace(&mut self.primitives, Vec::new()),
                     interners,
                 ),
@@ -3082,8 +3130,22 @@ fn create_prim_instance(
             data_handle,
             pic_index,
             segment_instance_index: SegmentInstanceIndex::INVALID,
         },
         clip_chain_id,
         spatial_node_index,
     )
 }
+
+fn create_clip_prim_instance(
+    spatial_node_index: SpatialNodeIndex,
+    clip_chain_id: ClipChainId,
+    kind: PrimitiveInstanceKind,
+) -> PrimitiveInstance {
+    PrimitiveInstance::new(
+        LayoutPoint::zero(),
+        LayoutRect::max_rect(),
+        kind,
+        clip_chain_id,
+        spatial_node_index,
+    )
+}
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -10,30 +10,30 @@ use crate::clip_scroll_tree::{ClipScroll
 use crate::display_list_flattener::{DisplayListFlattener};
 use crate::gpu_cache::{GpuCache, GpuCacheHandle};
 use crate::gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
 use crate::hit_test::{HitTester, HitTestingScene};
 #[cfg(feature = "replay")]
 use crate::hit_test::HitTestingSceneStats;
 use crate::internal_types::{FastHashMap, PlaneSplitter};
 use crate::picture::{PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex};
-use crate::picture::{RetainedTiles, TileCache, DirtyRegion, SurfaceRenderTasks};
-use crate::prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
+use crate::picture::{RetainedTiles, TileCacheInstance, DirtyRegion, SurfaceRenderTasks, SubpixelMode};
+use crate::prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer, PrimitiveVisibilityMask};
 #[cfg(feature = "replay")]
 use crate::prim_store::{PrimitiveStoreStats};
 use crate::profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use crate::render_backend::{DataStores, FrameStamp};
 use crate::render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskGraph, RenderTaskGraphCounters};
 use crate::resource_cache::{ResourceCache};
 use crate::scene::{ScenePipeline, SceneProperties};
 use crate::scene_builder::DocumentStats;
 use crate::segment::SegmentBuilder;
 use std::{f32, mem};
 use std::sync::Arc;
-use crate::tiling::{Frame, RenderPassKind, RenderTargetContext, RenderTarget};
+use crate::tiling::{Frame, RenderPassKind, RenderTargetContext};
 use crate::util::MaxRect;
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ChasePrimitive {
     Nothing,
@@ -57,16 +57,17 @@ pub struct FrameBuilderConfig {
     pub chase_primitive: ChasePrimitive,
     pub enable_picture_caching: bool,
     /// True if we're running tests (i.e. via wrench).
     pub testing: bool,
     pub gpu_supports_fast_clears: bool,
     pub gpu_supports_advanced_blend: bool,
     pub advanced_blend_is_coherent: bool,
     pub batch_lookback_count: usize,
+    pub background_color: Option<ColorF>,
 }
 
 /// A set of common / global resources that are retained between
 /// new display lists, such that any GPU cache handles can be
 /// persisted even when a new display list arrives.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct FrameGlobalResources {
     /// The image shader block for the most common / default
@@ -112,41 +113,41 @@ pub struct FrameBuilder {
     #[cfg_attr(feature = "capture", serde(skip))] //TODO
     pub hit_testing_scene: Arc<HitTestingScene>,
     pub config: FrameBuilderConfig,
     pub globals: FrameGlobalResources,
 }
 
 pub struct FrameVisibilityContext<'a> {
     pub clip_scroll_tree: &'a ClipScrollTree,
-    pub screen_world_rect: WorldRect,
+    pub global_screen_world_rect: WorldRect,
     pub global_device_pixel_scale: DevicePixelScale,
     pub surfaces: &'a [SurfaceInfo],
     pub debug_flags: DebugFlags,
     pub scene_properties: &'a SceneProperties,
     pub config: &'a FrameBuilderConfig,
 }
 
 pub struct FrameVisibilityState<'a> {
     pub clip_store: &'a mut ClipStore,
     pub resource_cache: &'a mut ResourceCache,
     pub gpu_cache: &'a mut GpuCache,
     pub scratch: &'a mut PrimitiveScratchBuffer,
-    pub tile_cache: Option<Box<TileCache>>,
+    pub tile_cache: Option<Box<TileCacheInstance>>,
     pub retained_tiles: &'a mut RetainedTiles,
     pub data_stores: &'a mut DataStores,
     pub clip_chain_stack: ClipChainStack,
     pub render_tasks: &'a mut RenderTaskGraph,
 }
 
 pub struct FrameBuildingContext<'a> {
     pub global_device_pixel_scale: DevicePixelScale,
     pub scene_properties: &'a SceneProperties,
     pub pipelines: &'a FastHashMap<PipelineId, Arc<ScenePipeline>>,
-    pub screen_world_rect: WorldRect,
+    pub global_screen_world_rect: WorldRect,
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub max_local_clip: LayoutRect,
     pub debug_flags: DebugFlags,
     pub fb_config: &'a FrameBuilderConfig,
 }
 
 pub struct FrameBuildingState<'a> {
     pub render_tasks: &'a mut RenderTaskGraph,
@@ -178,22 +179,22 @@ impl<'a> FrameBuildingState<'a> {
 }
 
 /// Immutable context of a picture when processing children.
 #[derive(Debug)]
 pub struct PictureContext {
     pub pic_index: PictureIndex,
     pub apply_local_clip_rect: bool,
     pub is_passthrough: bool,
-    pub is_composite: bool,
     pub surface_spatial_node_index: SpatialNodeIndex,
     pub raster_spatial_node_index: SpatialNodeIndex,
     /// The surface that this picture will render on.
     pub surface_index: SurfaceIndex,
     pub dirty_region_count: usize,
+    pub subpixel_mode: SubpixelMode,
 }
 
 /// Mutable state of a picture that gets modified when
 /// the children are processed.
 pub struct PictureState {
     pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
     pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
     pub map_pic_to_raster: SpaceMapper<PicturePixel, RasterPixel>,
@@ -221,16 +222,17 @@ impl FrameBuilder {
                 dual_source_blending_is_supported: false,
                 chase_primitive: ChasePrimitive::Nothing,
                 enable_picture_caching: false,
                 testing: false,
                 gpu_supports_fast_clears: false,
                 gpu_supports_advanced_blend: false,
                 advanced_blend_is_coherent: false,
                 batch_lookback_count: 0,
+                background_color: None,
             },
         }
     }
 
     /// Provide any cached surface tiles from the previous frame builder
     /// to a new frame builder. These will be consumed or dropped the
     /// first time a new frame builder creates a frame.
     pub fn set_retained_resources(
@@ -269,21 +271,19 @@ impl FrameBuilder {
         }
     }
 
     /// Destroy an existing frame builder. This is called just before
     /// a frame builder is replaced with a newly built scene.
     pub fn destroy(
         self,
         retained_tiles: &mut RetainedTiles,
-        clip_scroll_tree: &ClipScrollTree,
     ) -> FrameGlobalResources {
         self.prim_store.destroy(
             retained_tiles,
-            clip_scroll_tree,
         );
 
         // In general, the pending retained tiles are consumed by the frame
         // builder the first time a frame is built after a new scene has
         // arrived. However, if two scenes arrive in quick succession, the
         // frame builder may not have had a chance to build a frame and
         // consume the pending tiles. In this case, the pending tiles will
         // be lost, causing a full invalidation of the entire screen. To
@@ -293,17 +293,17 @@ impl FrameBuilder {
 
         self.globals
     }
 
     /// Compute the contribution (bounding rectangles, and resources) of layers and their
     /// primitives in screen space.
     fn build_layer_screen_rects_and_cull_layers(
         &mut self,
-        screen_world_rect: WorldRect,
+        global_screen_world_rect: WorldRect,
         clip_scroll_tree: &ClipScrollTree,
         pipelines: &FastHashMap<PipelineId, Arc<ScenePipeline>>,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskGraph,
         profile_counters: &mut FrameProfileCounters,
         global_device_pixel_scale: DevicePixelScale,
         scene_properties: &SceneProperties,
@@ -325,36 +325,48 @@ impl FrameBuilder {
         let root_spatial_node_index = clip_scroll_tree.root_reference_frame_index();
 
         const MAX_CLIP_COORD: f32 = 1.0e9;
 
         let frame_context = FrameBuildingContext {
             global_device_pixel_scale,
             scene_properties,
             pipelines,
-            screen_world_rect,
+            global_screen_world_rect,
             clip_scroll_tree,
             max_local_clip: LayoutRect::new(
                 LayoutPoint::new(-MAX_CLIP_COORD, -MAX_CLIP_COORD),
                 LayoutSize::new(2.0 * MAX_CLIP_COORD, 2.0 * MAX_CLIP_COORD),
             ),
             debug_flags,
             fb_config: &self.config,
         };
 
+        let root_render_task = RenderTask::new_picture(
+            RenderTaskLocation::Fixed(self.output_rect),
+            self.output_rect.size.to_f32(),
+            self.root_pic_index,
+            DeviceIntPoint::zero(),
+            UvRectKind::Rect,
+            ROOT_SPATIAL_NODE_INDEX,
+            global_device_pixel_scale,
+            PrimitiveVisibilityMask::all(),
+        );
+
+        let root_render_task_id = render_tasks.add(root_render_task);
+
         // Construct a dummy root surface, that represents the
         // main framebuffer surface.
         let root_surface = SurfaceInfo::new(
             ROOT_SPATIAL_NODE_INDEX,
             ROOT_SPATIAL_NODE_INDEX,
             0.0,
-            screen_world_rect,
+            global_screen_world_rect,
             clip_scroll_tree,
             global_device_pixel_scale,
-            true,
         );
         surfaces.push(root_surface);
 
         let mut retained_tiles = mem::replace(
             &mut self.pending_retained_tiles,
             RetainedTiles::new(),
         );
 
@@ -376,17 +388,17 @@ impl FrameBuilder {
         );
 
         {
             profile_marker!("UpdateVisibility");
 
             let visibility_context = FrameVisibilityContext {
                 global_device_pixel_scale,
                 clip_scroll_tree,
-                screen_world_rect,
+                global_screen_world_rect,
                 surfaces,
                 debug_flags,
                 scene_properties,
                 config: &self.config,
             };
 
             let mut visibility_state = FrameVisibilityState {
                 resource_cache,
@@ -398,80 +410,63 @@ impl FrameBuilder {
                 data_stores,
                 clip_chain_stack: ClipChainStack::new(),
                 render_tasks,
             };
 
             self.prim_store.update_visibility(
                 self.root_pic_index,
                 ROOT_SURFACE_INDEX,
+                &global_screen_world_rect,
                 &visibility_context,
                 &mut visibility_state,
             );
         }
 
-        {
-            profile_marker!("BlockOnResources");
-
-            resource_cache.block_until_all_resources_added(gpu_cache,
-                                                           render_tasks,
-                                                           texture_cache_profile);
-        }
-
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
             transforms: transform_palette,
             segment_builder: SegmentBuilder::new(),
             surfaces,
             dirty_region_stack: Vec::new(),
         };
 
-        let root_render_task = RenderTask::new_picture(
-            RenderTaskLocation::Fixed(self.output_rect),
-            self.output_rect.size.to_f32(),
-            self.root_pic_index,
-            DeviceIntPoint::zero(),
-            UvRectKind::Rect,
-            root_spatial_node_index,
-            root_spatial_node_index,
-            global_device_pixel_scale,
-        );
-
-        let root_render_task_id = frame_state.render_tasks.add(root_render_task);
         frame_state
             .surfaces
             .first_mut()
             .unwrap()
             .render_tasks = Some(SurfaceRenderTasks {
                 root: root_render_task_id,
                 port: root_render_task_id,
             });
 
         // Push a default dirty region which culls primitives
         // against the screen world rect, in absence of any
         // other dirty regions.
         let mut default_dirty_region = DirtyRegion::new();
         default_dirty_region.push(
-            frame_context.screen_world_rect,
+            frame_context.global_screen_world_rect,
+            PrimitiveVisibilityMask::all(),
         );
         frame_state.push_dirty_region(default_dirty_region);
 
         let (pic_context, mut pic_state, mut prim_list) = self
             .prim_store
             .pictures[self.root_pic_index.0]
             .take_context(
                 self.root_pic_index,
                 WorldRect::max_rect(),
                 root_spatial_node_index,
                 root_spatial_node_index,
                 ROOT_SURFACE_INDEX,
+                SubpixelMode::Allow,
                 &mut frame_state,
                 &frame_context,
             )
             .unwrap();
 
         {
             profile_marker!("PreparePrims");
 
@@ -491,16 +486,24 @@ impl FrameBuilder {
             prim_list,
             pic_context,
             pic_state,
             &mut frame_state,
         );
 
         frame_state.pop_dirty_region();
 
+        {
+            profile_marker!("BlockOnResources");
+
+            resource_cache.block_until_all_resources_added(gpu_cache,
+                                                           render_tasks,
+                                                           texture_cache_profile);
+        }
+
         Some(root_render_task_id)
     }
 
     pub fn build(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         stamp: FrameStamp,
@@ -608,22 +611,24 @@ impl FrameBuilder {
                     &mut deferred_resolves,
                     &self.clip_store,
                     &mut transform_palette,
                     &mut prim_headers,
                     &mut z_generator,
                 );
 
                 match pass.kind {
-                    RenderPassKind::MainFramebuffer { ref main_target, .. } => {
-                        has_texture_cache_tasks |= main_target.must_be_drawn();
-                    }
-                    RenderPassKind::OffScreen { ref texture_cache, ref color, .. } => {
+                    RenderPassKind::MainFramebuffer { .. } => {}
+                    RenderPassKind::OffScreen {
+                        ref texture_cache,
+                        ref picture_cache,
+                        ..
+                    } => {
                         has_texture_cache_tasks |= !texture_cache.is_empty();
-                        has_texture_cache_tasks |= color.must_be_drawn();
+                        has_texture_cache_tasks |= !picture_cache.is_empty();
                     }
                 }
             }
         }
 
         let gpu_cache_frame_id = gpu_cache.end_frame(gpu_cache_profile).frame_id();
 
         render_tasks.write_task_data();
--- a/gfx/wr/webrender/src/internal_types.rs
+++ b/gfx/wr/webrender/src/internal_types.rs
@@ -261,16 +261,18 @@ pub struct TextureCacheAllocation {
 pub struct TextureCacheAllocInfo {
     pub width: i32,
     pub height: i32,
     pub layer_count: i32,
     pub format: ImageFormat,
     pub filter: TextureFilter,
     /// Indicates whether this corresponds to one of the shared texture caches.
     pub is_shared_cache: bool,
+    /// If true, this texture requires a depth target.
+    pub has_depth: bool,
 }
 
 /// Sub-operation-specific information for allocation operations.
 #[derive(Debug)]
 pub enum TextureCacheAllocationKind {
     /// Performs an initial texture allocation.
     Alloc(TextureCacheAllocInfo),
     /// Reallocates the texture. The existing live texture with the same id
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,42 +1,43 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{MixBlendMode, PipelineId, PremultipliedColorF};
 use api::{PropertyBinding, PropertyBindingId};
-use api::{DebugFlags, RasterSpace, ColorF, ImageKey, ClipMode};
+use api::{DebugFlags, RasterSpace, ImageKey, ColorF};
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
-use crate::clip::{ClipChainId, ClipChainNode, ClipItem, ClipStore, ClipDataStore, ClipChainStack};
+use crate::clip::{ClipStore, ClipDataStore, ClipChainInstance};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX,
-    ClipScrollTree, CoordinateSystemId, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
+    ClipScrollTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace, CoordinateSystemId
 };
 use crate::debug_colors;
-use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D, Vector2D};
+use euclid::{vec3, TypedPoint2D, TypedScale, TypedSize2D, Vector2D, TypedRect};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::intern::ItemUid;
 use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter};
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
-use crate::prim_store::SpaceMapper;
+use crate::prim_store::{SpaceMapper, PrimitiveVisibilityMask, PointKey, PrimitiveTemplateKind};
 use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
-use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
-use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
+use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, RectangleKey};
+use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
 use crate::print_tree::PrintTreePrinter;
 use crate::render_backend::DataStores;
-use crate::render_task::{ClearMode, RenderTask, TileBlit};
+use crate::render_task::{ClearMode, RenderTask};
 use crate::render_task::{RenderTaskId, RenderTaskLocation, BlurTaskCache};
 use crate::resource_cache::ResourceCache;
 use crate::scene::SceneProperties;
 use crate::scene_builder::Interners;
+use crate::spatial_node::SpatialNodeType;
 use smallvec::SmallVec;
 use std::{mem, u16};
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::texture_cache::TextureCacheHandle;
 use crate::tiling::RenderTargetKind;
 use crate::util::{ComparableVec, TransformedRectKind, MatrixHelpers, MaxRect, scale_factors};
 use crate::filterdata::{FilterDataHandle};
 
@@ -45,92 +46,133 @@ use crate::filterdata::{FilterDataHandle
 
  * A number of primitives that are drawn onto the picture.
  * A composite operation describing how to composite this
    picture into its parent.
  * A configuration describing how to draw the primitives on
    this picture (e.g. in screen space or local space).
  */
 
+/// Specify whether a surface allows subpixel AA text rendering.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum SubpixelMode {
+    /// This surface allows subpixel AA text
+    Allow,
+    /// Subpixel AA text cannot be drawn on this surface
+    Deny,
+}
+
+/// A comparable / hashable version of a coordinate space mapping. Used to determine
+/// if a transform dependency for a tile has changed.
+#[derive(Debug, PartialEq, Clone)]
+enum TransformKey {
+    Local,
+    ScaleOffset {
+        scale_x: f32,
+        scale_y: f32,
+        offset_x: f32,
+        offset_y: f32,
+    },
+    Transform {
+        m: [f32; 16],
+    }
+}
+
+impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey {
+    fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey {
+        match transform {
+            CoordinateSpaceMapping::Local => {
+                TransformKey::Local
+            }
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                // TODO(gw): We should consider quantizing / rounding these values
+                //           to avoid invalidations due to floating point inaccuracies.
+                TransformKey::ScaleOffset {
+                    scale_x: scale_offset.scale.x,
+                    scale_y: scale_offset.scale.y,
+                    offset_x: scale_offset.offset.x,
+                    offset_y: scale_offset.offset.y,
+                }
+            }
+            CoordinateSpaceMapping::Transform(ref m) => {
+                // TODO(gw): We should consider quantizing / rounding these values
+                //           to avoid invalidations due to floating point inaccuracies.
+                TransformKey::Transform {
+                    m: m.to_row_major_array(),
+                }
+            }
+        }
+    }
+}
+
 /// Information about a picture that is pushed / popped on the
 /// PictureUpdateState during picture traversal pass.
 struct PictureInfo {
     /// The spatial node for this picture.
     _spatial_node_index: SpatialNodeIndex,
 }
 
 /// Stores a list of cached picture tiles that are retained
 /// between new scenes.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct RetainedTiles {
     /// The tiles retained between display lists.
     #[cfg_attr(feature = "capture", serde(skip))] //TODO
-    pub tiles: Vec<Tile>,
-    /// List of reference primitives that we will compare
-    /// to try and correlate the positioning of items
-    /// between display lists.
-    pub ref_prims: FastHashMap<ItemUid, WorldPoint>,
+    pub tiles: FastHashMap<TileKey, Tile>,
 }
 
 impl RetainedTiles {
     pub fn new() -> Self {
         RetainedTiles {
-            tiles: Vec::new(),
-            ref_prims: FastHashMap::default(),
+            tiles: FastHashMap::default(),
         }
     }
 
     /// Merge items from one retained tiles into another.
     pub fn merge(&mut self, other: RetainedTiles) {
         assert!(self.tiles.is_empty() || other.tiles.is_empty());
         self.tiles.extend(other.tiles);
-        self.ref_prims.extend(other.ref_prims);
     }
 }
 
 /// Unit for tile coordinates.
 #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub struct TileCoordinate;
 
 // Geometry types for tile coordinates.
 pub type TileOffset = TypedPoint2D<i32, TileCoordinate>;
 pub type TileSize = TypedSize2D<i32, TileCoordinate>;
-pub struct TileIndex(pub usize);
+pub type TileRect = TypedRect<i32, TileCoordinate>;
 
 /// The size in device pixels of a cached tile. The currently chosen
 /// size is arbitrary. We should do some profiling to find the best
 /// size for real world pages.
 ///
 /// Note that we use a separate, smaller size during wrench testing, so that
 /// we get tighter dirty rects and can do more meaningful invalidation
 /// tests.
-const TILE_SIZE_WIDTH: i32 = 1024;
-const TILE_SIZE_HEIGHT: i32 = 256;
-const TILE_SIZE_TESTING: i32 = 64;
-
-const FRAMES_BEFORE_PICTURE_CACHING: usize = 2;
-const MAX_DIRTY_RECTS: usize = 3;
+pub const TILE_SIZE_WIDTH: i32 = 1024;
+pub const TILE_SIZE_HEIGHT: i32 = 512;
 
 /// The maximum size per axis of a surface,
 ///  in WorldPixel coordinates.
 const MAX_SURFACE_SIZE: f32 = 4096.0;
 
-
-/// The maximum number of primitives to look for in a display
-/// list, trying to find unique primitives.
-const MAX_PRIMS_TO_SEARCH: usize = 128;
-
 /// Used to get unique tile IDs, even when the tile cache is
 /// destroyed between display lists / scenes.
 static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0);
 
 fn clamp(value: i32, low: i32, high: i32) -> i32 {
     value.max(low).min(high)
 }
 
+fn clampf(value: f32, low: f32, high: f32) -> f32 {
+    value.max(low).min(high)
+}
+
 /// Information about the state of an opacity binding.
 #[derive(Debug)]
 pub struct OpacityBindingInfo {
     /// The current value retrieved from dynamic scene properties.
     value: f32,
     /// True if it was changed (or is new) since the last frame build.
     changed: bool,
 }
@@ -148,139 +190,123 @@ impl From<PropertyBinding<f32>> for Opac
             PropertyBinding::Binding(key, _) => OpacityBinding::Binding(key.id),
             PropertyBinding::Value(value) => OpacityBinding::Value(value),
         }
     }
 }
 
 /// A stable ID for a given tile, to help debugging.
 #[derive(Debug, Copy, Clone, PartialEq)]
-struct TileId(usize);
+pub struct TileId(usize);
+
+#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
+pub struct TileKey {
+    /// The picture cache slice that this belongs to.
+    slice: usize,
+    /// Offset (in tile coords) of the tile within this slice.
+    offset: TileOffset,
+}
 
 /// Information about a cached tile.
 #[derive(Debug)]
 pub struct Tile {
-    /// The current world rect of thie tile.
-    world_rect: WorldRect,
+    /// The current world rect of this tile.
+    pub world_rect: WorldRect,
     /// The current local rect of this tile.
-    pub local_rect: LayoutRect,
-    /// The currently visible rect within this tile, updated per frame.
-    /// If None, this tile is not currently visible.
-    visible_rect: Option<WorldRect>,
-    /// The currently valid rect of the tile, used to invalidate
-    /// tiles that were only partially drawn.
-    valid_rect: WorldRect,
+    pub rect: PictureRect,
+    /// The local rect of the tile clipped to the overal picture local rect.
+    clipped_rect: PictureRect,
     /// Uniquely describes the content of this tile, in a way that can be
     /// (reasonably) efficiently hashed and compared.
-    descriptor: TileDescriptor,
+    pub descriptor: TileDescriptor,
     /// Handle to the cached texture for this tile.
     pub handle: TextureCacheHandle,
     /// If true, this tile is marked valid, and the existing texture
     /// cache handle can be used. Tiles are invalidated during the
     /// build_dirty_regions method.
-    is_valid: bool,
+    pub is_valid: bool,
     /// If true, the content on this tile is the same as last frame.
     is_same_content: bool,
-    /// The number of frames this tile has had the same content.
-    same_frames: usize,
     /// The tile id is stable between display lists and / or frames,
     /// if the tile is retained. Useful for debugging tile evictions.
-    id: TileId,
+    pub id: TileId,
     /// The set of transforms that affect primitives on this tile we
     /// care about. Stored as a set here, and then collected, sorted
     /// and converted to transform key values during post_update.
     transforms: FastHashSet<SpatialNodeIndex>,
-    /// A list of potentially important clips. We can't know if
-    /// they were important or can be discarded until we know the
-    /// tile cache bounding rect.
-    potential_clips: FastHashMap<RectangleKey, SpatialNodeIndex>,
-    /// If true, this tile should still be considered as part of
-    /// the dirty rect calculations.
-    consider_for_dirty_rect: bool,
+    /// Bitfield specifying the dirty region(s) that are relevant to this tile.
+    visibility_mask: PrimitiveVisibilityMask,
+    /// If true, the tile was determined to be opaque, which means blending
+    /// can be disabled when drawing it.
+    pub is_opaque: bool,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
     fn new(
         id: TileId,
     ) -> Self {
         Tile {
-            local_rect: LayoutRect::zero(),
+            rect: PictureRect::zero(),
+            clipped_rect: PictureRect::zero(),
             world_rect: WorldRect::zero(),
-            visible_rect: None,
-            valid_rect: WorldRect::zero(),
             handle: TextureCacheHandle::invalid(),
             descriptor: TileDescriptor::new(),
             is_same_content: false,
             is_valid: false,
-            same_frames: 0,
             transforms: FastHashSet::default(),
-            potential_clips: FastHashMap::default(),
             id,
-            consider_for_dirty_rect: false,
+            visibility_mask: PrimitiveVisibilityMask::empty(),
+            is_opaque: false,
         }
     }
 
     /// Clear the dependencies for a tile.
     fn clear(&mut self) {
         self.transforms.clear();
         self.descriptor.clear();
-        self.potential_clips.clear();
     }
 
     /// Invalidate a tile based on change in content. This
-    /// muct be called even if the tile is not currently
+    /// must be called even if the tile is not currently
     /// visible on screen. We might be able to improve this
     /// later by changing how ComparableVec is used.
     fn update_content_validity(&mut self) {
         // Check if the contents of the primitives, clips, and
         // other dependencies are the same.
-        self.is_same_content &= self.descriptor.is_same_content();
+        self.is_same_content &= self.descriptor.is_same_content(self.id);
         self.is_valid &= self.is_same_content;
     }
-
-    /// Update state related to whether a tile has a valid rect that
-    /// covers the required visible part of the tile.
-    fn update_rect_validity(&mut self, tile_bounding_rect: &WorldRect) {
-        // The tile is only valid if:
-        // - The content is the same *and*
-        // - The valid part of the tile includes the needed part.
-        self.is_valid &= self.valid_rect.contains_rect(tile_bounding_rect);
-
-        // Update count of how many times this tile has had the same content.
-        if !self.is_same_content {
-            self.same_frames = 0;
-        }
-        self.same_frames += 1;
-    }
 }
 
 /// Defines a key that uniquely identifies a primitive instance.
 #[derive(Debug, Clone, PartialEq)]
 pub struct PrimitiveDescriptor {
     /// Uniquely identifies the content of the primitive template.
     prim_uid: ItemUid,
     /// The origin in world space of this primitive.
-    origin: WorldPoint,
+    origin: PointKey,
+    /// The clip rect for this primitive. Included here in
+    /// dependencies since there is no entry in the clip chain
+    /// dependencies for the local clip rect.
+    prim_clip_rect: RectangleKey,
     /// The first clip in the clip_uids array of clips that affect this tile.
     first_clip: u16,
     /// The number of clips that affect this primitive instance.
     clip_count: u16,
-    /// The combined local clips + prim rect for this primitive.
-    world_culling_rect: WorldRect,
 }
 
 /// Uniquely describes the content of this tile, in a way that can be
 /// (reasonably) efficiently hashed and compared.
 #[derive(Debug)]
 pub struct TileDescriptor {
     /// List of primitive instance unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the primitive template, while
     /// the other parameters describe the clip chain and instance params.
-    prims: ComparableVec<PrimitiveDescriptor>,
+    pub prims: ComparableVec<PrimitiveDescriptor>,
 
     /// List of clip node unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the clip node.
     clip_uids: ComparableVec<ItemUid>,
 
     /// List of local offsets of the clip node origins. This
     /// ensures that if a clip node is supplied but has a different
     /// transform between frames that the tile is invalidated.
@@ -290,17 +316,17 @@ pub struct TileDescriptor {
     image_keys: ComparableVec<ImageKey>,
 
     /// The set of opacity bindings that this tile depends on.
     // TODO(gw): Ugh, get rid of all opacity binding support!
     opacity_bindings: ComparableVec<OpacityBinding>,
 
     /// List of the effects of transforms that we care about
     /// tracking for this tile.
-    transforms: ComparableVec<PointKey>,
+    transforms: ComparableVec<TransformKey>,
 }
 
 impl TileDescriptor {
     fn new() -> Self {
         TileDescriptor {
             prims: ComparableVec::new(),
             clip_uids: ComparableVec::new(),
             clip_vertices: ComparableVec::new(),
@@ -319,17 +345,17 @@ impl TileDescriptor {
         self.opacity_bindings.reset();
         self.image_keys.reset();
         self.transforms.reset();
     }
 
     /// Return true if the content of the tile is the same
     /// as last frame. This doesn't check validity of the
     /// tile based on the currently valid regions.
-    fn is_same_content(&self) -> bool {
+    fn is_same_content(&self, _id: TileId) -> bool {
         if !self.image_keys.is_valid() {
             return false;
         }
         if !self.opacity_bindings.is_valid() {
             return false;
         }
         if !self.clip_uids.is_valid() {
             return false;
@@ -346,87 +372,91 @@ impl TileDescriptor {
 
         true
     }
 }
 
 /// Stores both the world and devices rects for a single dirty rect.
 #[derive(Debug, Clone)]
 pub struct DirtyRegionRect {
+    /// World rect of this dirty region
     pub world_rect: WorldRect,
+    /// Bitfield for picture render tasks that draw this dirty region.
+    pub visibility_mask: PrimitiveVisibilityMask,
 }
 
 /// Represents the dirty region of a tile cache picture.
 #[derive(Debug, Clone)]
 pub struct DirtyRegion {
     /// The individual dirty rects of this region.
     pub dirty_rects: Vec<DirtyRegionRect>,
 
     /// The overall dirty rect, a combination of dirty_rects
-    pub combined: DirtyRegionRect,
+    pub combined: WorldRect,
 }
 
 impl DirtyRegion {
     /// Construct a new dirty region tracker.
-    pub fn new() -> Self {
+    pub fn new(
+    ) -> Self {
         DirtyRegion {
-            dirty_rects: Vec::with_capacity(MAX_DIRTY_RECTS),
-            combined: DirtyRegionRect {
-                world_rect: WorldRect::zero(),
-            },
+            dirty_rects: Vec::with_capacity(PrimitiveVisibilityMask::MAX_DIRTY_REGIONS),
+            combined: WorldRect::zero(),
         }
     }
 
     /// Reset the dirty regions back to empty
     pub fn clear(&mut self) {
         self.dirty_rects.clear();
-        self.combined = DirtyRegionRect {
-            world_rect: WorldRect::zero(),
-        }
+        self.combined = WorldRect::zero();
     }
 
     /// Push a dirty rect into this region
     pub fn push(
         &mut self,
         rect: WorldRect,
+        visibility_mask: PrimitiveVisibilityMask,
     ) {
         // Include this in the overall dirty rect
-        self.combined.world_rect = self.combined.world_rect.union(&rect);
+        self.combined = self.combined.union(&rect);
 
         // Store the individual dirty rect.
         self.dirty_rects.push(DirtyRegionRect {
             world_rect: rect,
+            visibility_mask,
         });
     }
 
-    /// Returns true if this region has no dirty rects
-    pub fn is_empty(&self) -> bool {
-        self.dirty_rects.is_empty()
+    /// Include another rect into an existing dirty region.
+    pub fn include_rect(
+        &mut self,
+        region_index: usize,
+        rect: WorldRect,
+    ) {
+        self.combined = self.combined.union(&rect);
+
+        let region = &mut self.dirty_rects[region_index];
+        region.world_rect = region.world_rect.union(&rect);
     }
 
-    /// Collapse all dirty rects into a single dirty rect.
-    pub fn collapse(&mut self) {
-        self.dirty_rects.clear();
-        self.dirty_rects.push(self.combined.clone());
-    }
-
+    // TODO(gw): This returns a heap allocated object. Perhaps we can simplify this
+    //           logic? Although - it's only used very rarely so it may not be an issue.
     pub fn inflate(
         &self,
         inflate_amount: f32,
     ) -> DirtyRegion {
         let mut dirty_rects = Vec::with_capacity(self.dirty_rects.len());
-        let mut combined = DirtyRegionRect {
-            world_rect: WorldRect::zero(),
-        };
+        let mut combined = WorldRect::zero();
 
         for rect in &self.dirty_rects {
             let world_rect = rect.world_rect.inflate(inflate_amount, inflate_amount);
-            combined.world_rect = combined.world_rect.union(&world_rect);
+            combined = combined.union(&world_rect);
             dirty_rects.push(DirtyRegionRect {
                 world_rect,
+                visibility_mask: rect.visibility_mask,
             });
         }
 
         DirtyRegion {
             dirty_rects,
             combined,
         }
     }
@@ -456,577 +486,332 @@ 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)
     }
 }
 
-/// A helper struct to build a (roughly) minimal set of dirty rectangles
-/// from a list of individual dirty rectangles. This minimizes the number
-/// of scissors rects and batch resubmissions that are needed.
-struct DirtyRegionBuilder<'a> {
-    tiles: &'a mut [Tile],
-    tile_count: TileSize,
-}
-
-impl<'a> DirtyRegionBuilder<'a> {
-    fn new(
-        tiles: &'a mut [Tile],
-        tile_count: TileSize,
-    ) -> Self {
-        DirtyRegionBuilder {
-            tiles,
-            tile_count,
-        }
-    }
-
-    fn tile_index(&self, x: i32, y: i32) -> usize {
-        (y * self.tile_count.width + x) as usize
-    }
-
-    fn is_dirty(&self, x: i32, y: i32) -> bool {
-        if x == self.tile_count.width || y == self.tile_count.height {
-            return false;
-        }
-
-        self.get_tile(x, y).consider_for_dirty_rect
-    }
-
-    fn get_tile(&self, x: i32, y: i32) -> &Tile {
-        &self.tiles[self.tile_index(x, y)]
-    }
-
-    fn get_tile_mut(&mut self, x: i32, y: i32) -> &mut Tile {
-        &mut self.tiles[self.tile_index(x, y)]
-    }
-
-    /// Return true if the entire column is dirty
-    fn column_is_dirty(&self, x: i32, y0: i32, y1: i32) -> bool {
-        for y in y0 .. y1 {
-            if !self.is_dirty(x, y) {
-                return false;
-            }
-        }
-
-        true
-    }
-
-    /// Push a dirty rect into the final region list.
-    fn push_dirty_rect(
-        &mut self,
-        x0: i32,
-        y0: i32,
-        x1: i32,
-        y1: i32,
-        dirty_region: &mut DirtyRegion,
-    ) {
-        // Construct the overall dirty rect by combining the visible
-        // parts of the dirty rects that were combined.
-        let mut dirty_world_rect = WorldRect::zero();
-
-        for y in y0 .. y1 {
-            for x in x0 .. x1 {
-                let tile = self.get_tile_mut(x, y);
-                tile.consider_for_dirty_rect = false;
-                if let Some(visible_rect) = tile.visible_rect {
-                    dirty_world_rect = dirty_world_rect.union(&visible_rect);
-                }
-            }
-        }
-
-        dirty_region.push(dirty_world_rect);
-    }
-
-    /// Simple sweep through the tile grid to try and coalesce individual
-    /// dirty rects into a smaller number of larger dirty rectangles.
-    fn build(&mut self, dirty_region: &mut DirtyRegion) {
-        for x0 in 0 .. self.tile_count.width {
-            for y0 in 0 .. self.tile_count.height {
-                let mut y1 = y0;
-
-                while self.is_dirty(x0, y1) {
-                    y1 += 1;
-                }
-
-                if y1 > y0 {
-                    let mut x1 = x0;
-
-                    while self.column_is_dirty(x1, y0, y1) {
-                        x1 += 1;
-                    }
-
-                    self.push_dirty_rect(x0, y0, x1, y1, dirty_region);
-                }
-            }
-        }
-    }
-}
-
 /// Represents a cache of tiles that make up a picture primitives.
-pub struct TileCache {
+pub struct TileCacheInstance {
+    /// Index of the tile cache / slice for this frame builder. It's determined
+    /// by the setup_picture_caching method during flattening, which splits the
+    /// picture tree into multiple slices. It's used as a simple input to the tile
+    /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed
+    /// between display lists - this seems very unlikely to occur on most pages, but
+    /// can be revisited if we ever notice that.
+    pub slice: usize,
     /// The positioning node for this tile cache.
-    spatial_node_index: SpatialNodeIndex,
-    /// List of tiles present in this picture (stored as a 2D array)
-    pub tiles: Vec<Tile>,
-    /// A helper struct to map local rects into world coords.
-    map_local_to_world: SpaceMapper<LayoutPixel, WorldPixel>,
-    /// A list of tiles to draw during batching.
-    pub tiles_to_draw: Vec<TileIndex>,
+    pub spatial_node_index: SpatialNodeIndex,
+    /// Hash of tiles present in this picture.
+    pub tiles: FastHashMap<TileKey, Tile>,
+    /// A helper struct to map local rects into surface coords.
+    map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
     /// List of opacity bindings, with some extra information
     /// about whether they changed since last frame.
     opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
     /// The current dirty region tracker for this picture.
     pub dirty_region: DirtyRegion,
-    /// The current world reference point that tiles are created around.
-    world_origin: WorldPoint,
-    /// Current size of tiles in world units.
-    world_tile_size: WorldSize,
-    /// Current number of tiles in the allocated grid.
-    tile_count: TileSize,
-    /// The current scroll offset for this frame builder. Reset when
-    /// a new scene arrives.
-    scroll_offset: Option<WorldVector2D>,
-    /// A list of blits from the framebuffer to be applied during this frame.
-    pub pending_blits: Vec<TileBlit>,
-    /// The current world bounding rect of this tile cache. This is used
-    /// to derive a local clip rect, such that we don't obscure in the
-    /// z-buffer any items placed earlier in the render order (such as
-    /// scroll bars in gecko, when the content overflows under the
-    /// scroll bar).
-    world_bounding_rect: WorldRect,
-    /// World space clip rect of the root clipping node. Every primitive
-    /// has this as the root of the clip chain attached to the primitive.
-    root_clip_rect: WorldRect,
-    /// List of reference primitive information used for
-    /// correlating the position between display lists.
-    reference_prims: ReferencePrimitiveList,
-    /// The root clip chain for this tile cache.
-    root_clip_chain_id: ClipChainId,
-    /// If true, this tile cache is enabled. For now, it doesn't
-    /// support tile caching if the surface is not the main framebuffer.
-    pub is_enabled: bool,
+    /// Current size of tiles in picture units.
+    tile_size: PictureSize,
+    /// Tile coords of the currently allocated grid.
+    tile_rect: TileRect,
+    /// Pre-calculated versions of the tile_rect above, used to speed up the
+    /// calculations in get_tile_coords_for_rect.
+    tile_bounds_p0: TileOffset,
+    tile_bounds_p1: TileOffset,
+    /// Local rect (unclipped) of the picture this cache covers.
+    pub local_rect: PictureRect,
     /// Local clip rect for this tile cache.
-    pub local_clip_rect: LayoutRect,
+    pub local_clip_rect: PictureRect,
+    /// If true, the entire tile cache was determined to be opaque. This allows
+    /// WR to enable subpixel AA text rendering for text runs on this slice.
+    pub is_opaque: bool,
+    /// A list of tiles that are valid and visible, which should be drawn to the main scene.
+    pub tiles_to_draw: Vec<TileKey>,
+    /// The world space viewport that this tile cache draws into.
+    /// Any clips outside this viewport can be ignored (and must be removed so that
+    /// we can draw outside the bounds of the viewport).
+    pub world_viewport_rect: WorldRect,
+    /// 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>,
 }
 
-/// Stores information about a primitive in the cache that we will
-/// try to use to correlate positions between display lists.
-#[derive(Clone)]
-struct ReferencePrimitive {
-    uid: ItemUid,
-    local_pos: LayoutPoint,
-    spatial_node_index: SpatialNodeIndex,
-    ref_count: usize,
-}
-
-/// A list of primitive with uids that only exist once in a display
-/// list. Used to obtain reference points to correlate the offset
-/// between two similar display lists.
-struct ReferencePrimitiveList {
-    ref_prims: Vec<ReferencePrimitive>,
-}
-
-impl ReferencePrimitiveList {
-    fn new(
-        prim_instances: &[PrimitiveInstance],
-        pictures: &[PicturePrimitive],
+impl TileCacheInstance {
+    pub fn new(
+        slice: usize,
+        spatial_node_index: SpatialNodeIndex,
+        background_color: Option<ColorF>,
     ) -> Self {
-        let mut map = FastHashMap::default();
-        let mut search_count = 0;
-
-        // Collect a set of primitives that we can
-        // potentially use for correlation.
-        collect_ref_prims(
-            prim_instances,
-            pictures,
-            &mut map,
-            &mut search_count,
-        );
-
-        // Select only primitives where the uid is unique
-        // in the display list, giving the best chance
-        // of finding correct correlations.
-        let ref_prims = map.values().filter(|prim| {
-            prim.ref_count == 1
-        }).cloned().collect();
-
-        ReferencePrimitiveList {
-            ref_prims,
+        TileCacheInstance {
+            slice,
+            spatial_node_index,
+            tiles: FastHashMap::default(),
+            map_local_to_surface: SpaceMapper::new(
+                ROOT_SPATIAL_NODE_INDEX,
+                PictureRect::zero(),
+            ),
+            opacity_bindings: FastHashMap::default(),
+            dirty_region: DirtyRegion::new(),
+            tile_size: PictureSize::zero(),
+            tile_rect: TileRect::zero(),
+            tile_bounds_p0: TileOffset::zero(),
+            tile_bounds_p1: TileOffset::zero(),
+            local_rect: PictureRect::zero(),
+            local_clip_rect: PictureRect::zero(),
+            is_opaque: false,
+            tiles_to_draw: Vec::new(),
+            world_viewport_rect: WorldRect::zero(),
+            surface_index: SurfaceIndex(0),
+            background_color,
         }
     }
-}
-
-/// Collect a sample of primitives from the prim list that can
-/// be used to correlate positions.
-fn collect_ref_prims(
-    prim_instances: &[PrimitiveInstance],
-    pictures: &[PicturePrimitive],
-    map: &mut FastHashMap<ItemUid, ReferencePrimitive>,
-    search_count: &mut usize,
-) {
-    for prim_instance in prim_instances {
-        if *search_count > MAX_PRIMS_TO_SEARCH {
-            return;
+
+    /// Returns true if this tile cache is considered opaque.
+    pub fn is_opaque(&self) -> bool {
+        // If we detected an explicit background rect that is opaque and covers all tiles.
+        if self.is_opaque {
+            return true;
         }
 
-        match prim_instance.kind {
-            PrimitiveInstanceKind::Picture { pic_index, .. } => {
-                collect_ref_prims(
-                    &pictures[pic_index.0].prim_list.prim_instances,
-                    pictures,
-                    map,
-                    search_count,
-                );
-            }
-            _ => {
-                let uid = prim_instance.uid();
-
-                let entry = map.entry(uid).or_insert_with(|| {
-                    ReferencePrimitive {
-                        uid,
-                        local_pos: prim_instance.prim_origin,
-                        spatial_node_index: prim_instance.spatial_node_index,
-                        ref_count: 0,
-                    }
-                });
-                entry.ref_count += 1;
-
-                *search_count = *search_count + 1;
-            }
-        }
-    }
-}
-
-impl TileCache {
-    pub fn new(
-        spatial_node_index: SpatialNodeIndex,
-        prim_instances: &[PrimitiveInstance],
-        root_clip_chain_id: ClipChainId,
-        pictures: &[PicturePrimitive],
-    ) -> Self {
-        // Build the list of reference primitives
-        // for this picture cache.
-        let reference_prims = ReferencePrimitiveList::new(
-            prim_instances,
-            pictures,
-        );
-
-        TileCache {
-            spatial_node_index,
-            tiles: Vec::new(),
-            map_local_to_world: SpaceMapper::new(
-                ROOT_SPATIAL_NODE_INDEX,
-                WorldRect::zero(),
-            ),
-            tiles_to_draw: Vec::new(),
-            opacity_bindings: FastHashMap::default(),
-            dirty_region: DirtyRegion::new(),
-            world_origin: WorldPoint::zero(),
-            world_tile_size: WorldSize::zero(),
-            tile_count: TileSize::zero(),
-            scroll_offset: None,
-            pending_blits: Vec::new(),
-            world_bounding_rect: WorldRect::zero(),
-            root_clip_rect: WorldRect::max_rect(),
-            reference_prims,
-            root_clip_chain_id,
-            is_enabled: true,
-            local_clip_rect: LayoutRect::zero(),
+        // If known opaque due to background clear color and being the first slice.
+        // The background_color will only be Some(..) if this is the first slice.
+        match self.background_color {
+            Some(color) => color.a >= 1.0,
+            None => false
         }
     }
 
     /// Get the tile coordinates for a given rectangle.
     fn get_tile_coords_for_rect(
         &self,
-        rect: &WorldRect,
+        rect: &PictureRect,
     ) -> (TileOffset, TileOffset) {
-        // Translate the rectangle into the virtual tile space
-        let origin = rect.origin - self.world_origin;
-
         // Get the tile coordinates in the picture space.
         let mut p0 = TileOffset::new(
-            (origin.x / self.world_tile_size.width).floor() as i32,
-            (origin.y / self.world_tile_size.height).floor() as i32,
+            (rect.origin.x / self.tile_size.width).floor() as i32,
+            (rect.origin.y / self.tile_size.height).floor() as i32,
         );
 
         let mut p1 = TileOffset::new(
-            ((origin.x + rect.size.width) / self.world_tile_size.width).ceil() as i32,
-            ((origin.y + rect.size.height) / self.world_tile_size.height).ceil() as i32,
+            ((rect.origin.x + rect.size.width) / self.tile_size.width).ceil() as i32,
+            ((rect.origin.y + rect.size.height) / self.tile_size.height).ceil() as i32,
         );
 
         // Clamp the tile coordinates here to avoid looping over irrelevant tiles later on.
-        p0.x = clamp(p0.x, 0, self.tile_count.width);
-        p0.y = clamp(p0.y, 0, self.tile_count.height);
-        p1.x = clamp(p1.x, 0, self.tile_count.width);
-        p1.y = clamp(p1.y, 0, self.tile_count.height);
+        p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+        p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
+        p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+        p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
 
         (p0, p1)
     }
 
     /// Update transforms, opacity bindings and tile rects.
     pub fn pre_update(
         &mut self,
-        pic_rect: LayoutRect,
+        pic_rect: PictureRect,
+        surface_index: SurfaceIndex,
         frame_context: &FrameVisibilityContext,
         frame_state: &mut FrameVisibilityState,
-        surface_index: SurfaceIndex,
-    ) {
-        // If the tile cache is the first surface on the root
-        // surface, then we can enable it. If the client has
-        // requested caching on an offscreen surface, we will
-        // need to disable it (for now).
-        self.is_enabled = surface_index == SurfaceIndex(1);
-        if !self.is_enabled {
-            // TODO(gw): It's technically possible that this tile cache
-            //           might have been enabled in a valid state, and
-            //           then got an offscreen surface. In this case,
-            //           there may be some pre-cached tiles still existing.
-            //           They will expire from the texture cache as normal,
-            //           but we should check this path a bit more carefully
-            //           to see if any other memory should be freed.
-            return;
-        }
-
-        let DeviceIntSize { width: tile_width, height: tile_height, _unit: _ } =
-            Self::tile_dimensions(frame_context.config.testing);
-
-        // Work out the scroll offset to apply to the world reference point.
-        let scroll_offset_point = frame_context.clip_scroll_tree
-            .get_world_transform(self.spatial_node_index)
-            .inverse_project_2d_origin()
-            .unwrap_or_else(LayoutPoint::zero);
-
-        let scroll_offset = WorldVector2D::new(scroll_offset_point.x, scroll_offset_point.y);
-        let scroll_delta = match self.scroll_offset {
-            Some(prev) => prev - scroll_offset,
-            None => WorldVector2D::zero(),
-        };
-        self.scroll_offset = Some(scroll_offset);
-
-        // Pull any retained tiles from the previous scene.
-        let world_offset = if frame_state.retained_tiles.tiles.is_empty() {
-            None
-        } else {
-            assert!(self.tiles.is_empty());
-            self.tiles = mem::replace(&mut frame_state.retained_tiles.tiles, Vec::new());
-
-            // Get the positions of the reference primitives for this
-            // new display list.
-            let mut new_prim_map = FastHashMap::default();
-            build_ref_prims(
-                &self.reference_prims.ref_prims,
-                &mut new_prim_map,
-                frame_context.clip_scroll_tree,
-            );
-
-            // Attempt to correlate them to work out which offset to apply.
-            correlate_prim_maps(
-                &frame_state.retained_tiles.ref_prims,
-                &new_prim_map,
-            )
-        }.unwrap_or_else(WorldVector2D::zero);
-
-        // Assume no tiles are valid to draw by default
-        self.tiles_to_draw.clear();
-
-        self.map_local_to_world = SpaceMapper::new(
-            ROOT_SPATIAL_NODE_INDEX,
-            frame_context.screen_world_rect,
+    ) -> WorldRect {
+        let tile_width = TILE_SIZE_WIDTH;
+        let tile_height = TILE_SIZE_HEIGHT;
+        self.surface_index = surface_index;
+
+        self.map_local_to_surface = SpaceMapper::new(
+            self.spatial_node_index,
+            PictureRect::from_untyped(&pic_rect.to_untyped()),
         );
 
-        let world_mapper = SpaceMapper::new_with_target(
+        let pic_to_world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
-            frame_context.screen_world_rect,
+            frame_context.global_screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
+        let spatial_node = &frame_context
+            .clip_scroll_tree
+            .spatial_nodes[self.spatial_node_index.0 as usize];
+        let (viewport_rect, viewport_spatial_node_index) = match spatial_node.node_type {
+            SpatialNodeType::ScrollFrame(ref info) => {
+                (info.viewport_rect, spatial_node.parent.unwrap())
+            }
+            SpatialNodeType::StickyFrame(..) => {
+                unreachable!();
+            }
+            SpatialNodeType::ReferenceFrame(..) => {
+                assert_eq!(self.spatial_node_index, ROOT_SPATIAL_NODE_INDEX);
+                (LayoutRect::max_rect(), ROOT_SPATIAL_NODE_INDEX)
+            }
+        };
+
+        let viewport_to_world_mapper = SpaceMapper::new_with_target(
+            ROOT_SPATIAL_NODE_INDEX,
+            viewport_spatial_node_index,
+            frame_context.global_screen_world_rect,
+            frame_context.clip_scroll_tree,
+        );
+        self.world_viewport_rect = viewport_to_world_mapper
+            .map(&viewport_rect)
+            .expect("bug: unable to map viewport to world space");
+
+        // TODO(gw): This is a reverse mapping. It should always work since we know
+        //           that this path only runs for slices in the root coordinate system.
+        //           But perhaps we should assert that?
+        // TODO(gw): We could change to directly use the ScaleOffset in content_transform
+        //           which would make this clearer that we know the coordinate systems are the
+        //           same and that it's a safe / exact conversion.
+        self.map_local_to_surface.set_target_spatial_node(
+            viewport_spatial_node_index,
+            frame_context.clip_scroll_tree,
+        );
+        let local_viewport_rect = self
+            .map_local_to_surface
+            .map(&viewport_rect)
+            .expect("bug: unable to map to local viewport rect");
+
+        self.local_rect = pic_rect;
+        self.local_clip_rect = local_viewport_rect;
+
         // Do a hacky diff of opacity binding values from the last frame. This is
         // used later on during tile invalidation tests.
         let current_properties = frame_context.scene_properties.float_properties();
         let old_properties = mem::replace(&mut self.opacity_bindings, FastHashMap::default());
 
         for (id, value) in current_properties {
             let changed = match old_properties.get(id) {
                 Some(old_property) => !old_property.value.approx_eq(value),
                 None => true,
             };
             self.opacity_bindings.insert(*id, OpacityBindingInfo {
                 value: *value,
                 changed,
             });
         }
 
-        // Map the picture rect to world and device space and work out the tiles
-        // that we need in order to ensure the screen is covered. We haven't done
-        // any snapping yet, so we need to round out in device space to ensure we
-        // cover all pixels the picture may touch.
-        let pic_device_rect = {
-            let unsnapped_world_rect = world_mapper
-                .map(&pic_rect)
-                .expect("bug: unable to map picture rect to world");
-            (unsnapped_world_rect * frame_context.global_device_pixel_scale)
-                .round_out()
-        };
-        let pic_world_rect = pic_device_rect / frame_context.global_device_pixel_scale;
-
-        // If the bounding rect of the picture to cache doesn't intersect with
-        // the visible world rect at all, just take the screen world rect as
-        // a reference for the area to create tiles for. This allows existing
-        // tiles to be retained in case they are still valid if / when they
-        // get scrolled back onto the screen.
-
-        let needed_world_rect = frame_context
-            .screen_world_rect
-            .intersection(&pic_world_rect)
-            .unwrap_or(frame_context.screen_world_rect);
-
-        // Get a reference point that serves as an origin that all tiles we create
-        // must be aligned to. This ensures that tiles get reused correctly between
-        // scrolls and display list changes, even with the different local coord
-        // systems that gecko supplies.
-        let mut world_ref_point = if self.tiles.is_empty() {
-            needed_world_rect.origin.floor()
-        } else {
-            self.tiles[0].world_rect.origin + world_offset
-        };
-
-        // Apply the scroll delta so that existing tiles still get used.
-        world_ref_point += scroll_delta;
-
-        // Work out the required device rect that we need to cover the screen,
-        // given the world reference point constraint.
-        let device_ref_point = world_ref_point * frame_context.global_device_pixel_scale;
-        let device_world_rect = frame_context.screen_world_rect * frame_context.global_device_pixel_scale;
-        let needed_device_rect = pic_device_rect
-            .intersection(&device_world_rect)
-            .unwrap_or(device_world_rect);
-
-        // Expand the needed device rect vertically by a small number of tiles. This
-        // ensures that as tiles are scrolled in/out of view, they are retained for
-        // a while before being discarded.
-        // TODO(gw): On some pages it might be worth also inflating horizontally.
-        //           (is this locale specific?). It might be possible to make a good
-        //           guess based on the size of the picture rect for the tile cache.
-        let needed_device_rect = needed_device_rect.inflate(
-            0.0,
-            3.0 * tile_height as f32,
-        );
-
-        let p0 = needed_device_rect.origin;
-        let p1 = needed_device_rect.bottom_right();
-
-        let p0 = DevicePoint::new(
-            device_ref_point.x + ((p0.x - device_ref_point.x) / tile_width as f32).floor() * tile_width as f32,
-            device_ref_point.y + ((p0.y - device_ref_point.y) / tile_height as f32).floor() * tile_height as f32,
-        );
-
-        let p1 = DevicePoint::new(
-            device_ref_point.x + ((p1.x - device_ref_point.x) / tile_width as f32).ceil() * tile_width as f32,
-            device_ref_point.y + ((p1.y - device_ref_point.y) / tile_height as f32).ceil() * tile_height as f32,
-        );
-
-        // And now the number of tiles from that device rect.
-        let x_tiles = ((p1.x - p0.x) / tile_width as f32).round() as i32;
-        let y_tiles = ((p1.y - p0.y) / tile_height as f32).round() as i32;
-
-        // Step through any old tiles, and retain them if we can. They are keyed only on
-        // the (scroll adjusted) world position, relying on the descriptor content checks
-        // later to invalidate them if the content has changed.
-        let mut old_tiles = FastHashMap::default();
-        for tile in self.tiles.drain(..) {
-            let tile_device_pos = (tile.world_rect.origin + scroll_delta) * frame_context.global_device_pixel_scale;
-            let key = (
-                (tile_device_pos.x + world_offset.x).round() as i32,
-                (tile_device_pos.y + world_offset.y).round() as i32,
-            );
-            old_tiles.insert(key, tile);
-        }
-
-        // Store parameters about the current tiling rect for use during dependency updates.
-        self.world_origin = WorldPoint::new(
-            p0.x / frame_context.global_device_pixel_scale.0,
-            p0.y / frame_context.global_device_pixel_scale.0,
-        );
-        self.world_tile_size = WorldSize::new(
+        let world_tile_size = WorldSize::new(
             tile_width as f32 / frame_context.global_device_pixel_scale.0,
             tile_height as f32 / frame_context.global_device_pixel_scale.0,
         );
-        self.tile_count = TileSize::new(x_tiles, y_tiles);
-
-        // Step through each tile and try to retain an old tile from the
-        // previous frame, and update bounding rects.
-        for y in 0 .. y_tiles {
-            for x in 0 .. x_tiles {
-                let px = p0.x + x as f32 * tile_width as f32;
-                let py = p0.y + y as f32 * tile_height as f32;
-                let key = (px.round() as i32, py.round() as i32);
-
-                let mut tile = match old_tiles.remove(&key) {
-                    Some(tile) => tile,
-                    None => {
+
+        // We know that this is an exact rectangle, since we (for now) only support tile
+        // caches where the scroll root is in the root coordinate system.
+        let local_tile_rect = pic_to_world_mapper
+            .unmap(&WorldRect::new(WorldPoint::zero(), world_tile_size))
+            .expect("bug: unable to get local tile rect");
+
+        self.tile_size = local_tile_rect.size;
+
+        let screen_rect_in_pic_space = pic_to_world_mapper
+            .unmap(&frame_context.global_screen_world_rect)
+            .expect("unable to unmap screen rect");
+
+        let visible_rect_in_pic_space = screen_rect_in_pic_space
+            .intersection(&self.local_clip_rect)
+            .unwrap_or(PictureRect::zero());
+
+        // Inflate the needed rect a bit, so that we retain tiles that we have drawn
+        // but have just recently gone off-screen. This means that we avoid re-drawing
+        // tiles if the user is scrolling up and down small amounts, at the cost of
+        // a bit of extra texture memory.
+        let desired_rect_in_pic_space = visible_rect_in_pic_space
+            .inflate(0.0, 3.0 * self.tile_size.height);
+
+        let needed_rect_in_pic_space = desired_rect_in_pic_space
+            .intersection(&pic_rect)
+            .unwrap_or(PictureRect::zero());
+
+        let p0 = needed_rect_in_pic_space.origin;
+        let p1 = needed_rect_in_pic_space.bottom_right();
+
+        let x0 = (p0.x / local_tile_rect.size.width).floor() as i32;
+        let x1 = (p1.x / local_tile_rect.size.width).ceil() as i32;
+
+        let y0 = (p0.y / local_tile_rect.size.height).floor() as i32;
+        let y1 = (p1.y / local_tile_rect.size.height).ceil() as i32;
+
+        let x_tiles = x1 - x0;
+        let y_tiles = y1 - y0;
+        self.tile_rect = TileRect::new(
+            TileOffset::new(x0, y0),
+            TileSize::new(x_tiles, y_tiles),
+        );
+        // This is duplicated information from tile_rect, but cached here to avoid
+        // redundant calculations during get_tile_coords_for_rect
+        self.tile_bounds_p0 = TileOffset::new(x0, y0);
+        self.tile_bounds_p1 = TileOffset::new(x1, y1);
+
+        // TODO(gw): Tidy this up as we add better support for retaining
+        //           slices and sub-grid dirty areas.
+        let mut keys = Vec::new();
+        for key in frame_state.retained_tiles.tiles.keys() {
+            if key.slice == self.slice {
+                keys.push(*key);
+            }
+        }
+        for key in keys {
+            self.tiles.insert(key, frame_state.retained_tiles.tiles.remove(&key).unwrap());
+        }
+
+        let mut old_tiles = mem::replace(
+            &mut self.tiles,
+            FastHashMap::default(),
+        );
+
+        let mut world_culling_rect = WorldRect::zero();
+
+        for y in y0 .. y1 {
+            for x in x0 .. x1 {
+                let key = TileKey {
+                    offset: TileOffset::new(x, y),
+                    slice: self.slice,
+                };
+
+                let mut tile = old_tiles
+                    .remove(&key)
+                    .unwrap_or_else(|| {
                         let next_id = TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed));
                         Tile::new(next_id)
-                    }
-                };
-
-                tile.world_rect = WorldRect::new(
-                    WorldPoint::new(
-                        px / frame_context.global_device_pixel_scale.0,
-                        py / frame_context.global_device_pixel_scale.0,
+                    });
+
+                tile.rect = PictureRect::new(
+                    PicturePoint::new(
+                        x as f32 * self.tile_size.width,
+                        y as f32 * self.tile_size.height,
                     ),
-                    self.world_tile_size,
+                    self.tile_size,
                 );
 
-                tile.local_rect = world_mapper
-                    .unmap(&tile.world_rect)
-                    .expect("bug: can't unmap world rect");
-
-                tile.visible_rect = tile.world_rect.intersection(&frame_context.screen_world_rect);
-
-                self.tiles.push(tile);
-            }
-        }
-
-        if !old_tiles.is_empty() {
-            // TODO(gw): Should we explicitly drop the tile texture cache handles here?
-        }
-
-        self.world_bounding_rect = WorldRect::zero();
-        self.root_clip_rect = WorldRect::max_rect();
-
-        // Calculate the world space of the root clip node, that every primitive has
-        // at the root of its clip chain (this is enforced by the per-pipeline-root
-        // clip node added implicitly during display list flattening). Doing it once
-        // here saves doing it for every primitive during update_prim_dependencies.
-        if self.root_clip_chain_id != ClipChainId::NONE {
-            let root_clip_chain_node = &frame_state
-                .clip_store
-                .clip_chain_nodes[self.root_clip_chain_id.0 as usize];
-            let root_clip_node = &frame_state
-                .data_stores
-                .clip[root_clip_chain_node.handle];
-            if let Some(clip_rect) = root_clip_node.item.get_local_clip_rect(root_clip_chain_node.local_pos) {
-                self.map_local_to_world.set_target_spatial_node(
-                    root_clip_chain_node.spatial_node_index,
-                    frame_context.clip_scroll_tree,
-                );
-
-                if let Some(world_clip_rect) = self.map_local_to_world.map(&clip_rect) {
-                    self.root_clip_rect = world_clip_rect;
-                }
+                tile.clipped_rect = tile.rect
+                    .intersection(&self.local_rect)
+                    .unwrap_or(PictureRect::zero());
+
+                tile.world_rect = pic_to_world_mapper
+                    .map(&tile.rect)
+                    .expect("bug: map local tile rect");
+
+                world_culling_rect = world_culling_rect.union(&tile.world_rect);
+
+                self.tiles.insert(key, tile);
             }
         }
 
         // Do tile invalidation for any dependencies that we know now.
-        for tile in &mut self.tiles {
+        for (_, tile) in &mut self.tiles {
             // Start frame assuming that the tile has the same content.
             tile.is_same_content = true;
+            // Reset the opacity of the tile to false, since it may change each frame.
+            // The primitive dependency updates will determine if it is opaque again.
+            tile.is_opaque = false;
 
             // Content has changed if any images have changed
             for image_key in tile.descriptor.image_keys.items() {
                 if frame_state.resource_cache.is_image_dirty(*image_key) {
                     tile.is_same_content = false;
                     break;
                 }
             }
@@ -1044,75 +829,68 @@ impl TileCache {
                     }
                 }
             }
 
             // Clear any dependencies so that when we rebuild them we
             // can compare if the tile has the same content.
             tile.clear();
         }
+
+        world_culling_rect
     }
 
     /// Update the dependencies for each tile for a given primitive instance.
     pub fn update_prim_dependencies(
         &mut self,
         prim_instance: &PrimitiveInstance,
-        clip_chain_stack: &ClipChainStack,
-        prim_rect: LayoutRect,
+        prim_clip_chain: Option<&ClipChainInstance>,
+        local_prim_rect: LayoutRect,
         clip_scroll_tree: &ClipScrollTree,
         data_stores: &DataStores,
-        clip_chain_nodes: &[ClipChainNode],
+        clip_store: &ClipStore,
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
+        surface_index: SurfaceIndex,
     ) -> bool {
-        // If the tile cache is disabled, there's no need to update
-        // the primitive dependencies.
-        if !self.is_enabled {
-            // Return true to signal that we didn't early cull this primitive.
-            return true;
-        }
-
-        self.map_local_to_world.set_target_spatial_node(
+        self.map_local_to_surface.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
-        // Map the primitive local rect into world space.
-        let world_rect = match self.map_local_to_world.map(&prim_rect) {
+        // 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,
         };
 
         // If the rect is invalid, no need to create dependencies.
-        if world_rect.size.width <= 0.0 || world_rect.size.height <= 0.0 {
+        if prim_rect.size.is_empty_or_negative() {
             return false;
         }
 
         // Get the tile coordinates in the picture space.
-        let (p0, p1) = self.get_tile_coords_for_rect(&world_rect);
+        let (p0, p1) = self.get_tile_coords_for_rect(&prim_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;
         }
 
         // Build the list of resources that this primitive has dependencies on.
         let mut opacity_bindings: SmallVec<[OpacityBinding; 4]> = SmallVec::new();
         let mut clip_chain_uids: SmallVec<[ItemUid; 8]> = SmallVec::new();
-        let mut clip_vertices: SmallVec<[WorldPoint; 8]> = SmallVec::new();
+        let mut clip_vertices: SmallVec<[LayoutPoint; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
         let mut clip_spatial_nodes = FastHashSet::default();
-
-        // TODO(gw): We only care about world clip rects that don't have the main
-        //           scroll root as an ancestor. It may be a worthwhile optimization
-        //           to check for these and skip them.
-        let mut world_clips: SmallVec<[(RectangleKey, SpatialNodeIndex); 4]> = SmallVec::default();
+        let mut opaque_rect = None;
+        let mut prim_clip_rect = PictureRect::zero();
 
         // Some primitives can not be cached (e.g. external video images)
         let is_cacheable = prim_instance.is_cacheable(
             &data_stores,
             resource_cache,
         );
 
         // For pictures, we don't (yet) know the valid clip rect, so we can't correctly
@@ -1121,28 +899,60 @@ impl TileCache {
         // the clip bounds of the picture. Excluding them from the bounding rect here
         // fixes any correctness issues (the clips themselves are considered when we
         // consider the bounds of the primitives that are *children* of the picture),
         // however it does potentially result in some un-necessary invalidations of a
         // tile (in cases where the picture local rect affects the tile, but the clip
         // rect eventually means it doesn't affect that tile).
         // TODO(gw): Get picture clips earlier (during the initial picture traversal
         //           pass) so that we can calculate these correctly.
-        let include_clip_rect = match prim_instance.kind {
+        let clip_by_tile = match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index,.. } => {
                 // Pictures can depend on animated opacity bindings.
                 let pic = &pictures[pic_index.0];
                 if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.requested_composite_mode {
                     opacity_bindings.push(binding.into());
                 }
 
                 false
             }
-            PrimitiveInstanceKind::Rectangle { opacity_binding_index, .. } => {
-                if opacity_binding_index != OpacityBindingIndex::INVALID {
+            PrimitiveInstanceKind::Rectangle { data_handle, opacity_binding_index, .. } => {
+                if opacity_binding_index == OpacityBindingIndex::INVALID {
+                    // Check a number of conditions to see if we can consider this
+                    // primitive as an opaque 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.
+                    // Specifically, we currently require:
+                    //  - No opacity binding (to avoid resolving the opacity here).
+                    //  - Color.a >= 1.0 (the primitive is opaque).
+                    //  - Same coord system as picture cache (ensures rects are axis-aligned).
+                    //  - No clip masks exist.
+
+                    let on_picture_surface = surface_index == self.surface_index;
+
+                    let prim_is_opaque = match data_stores.prim[data_handle].kind {
+                        PrimitiveTemplateKind::Rectangle { ref color, .. } => color.a >= 1.0,
+                        _ => unreachable!(),
+                    };
+
+                    let same_coord_system = {
+                        let prim_spatial_node = &clip_scroll_tree
+                            .spatial_nodes[prim_instance.spatial_node_index.0 as usize];
+                        let surface_spatial_node = &clip_scroll_tree
+                            .spatial_nodes[self.spatial_node_index.0 as usize];
+
+                        prim_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id
+                    };
+
+                    if let Some(ref clip_chain) = prim_clip_chain {
+                        if prim_is_opaque && same_coord_system && !clip_chain.needs_mask && on_picture_surface {
+                            opaque_rect = Some(clip_chain.pic_clip_rect);
+                        }
+                    };
+                } else {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         opacity_bindings.push(OpacityBinding::from(*binding));
                     }
                 }
 
                 true
             }
@@ -1154,466 +964,295 @@ impl TileCache {
                 if opacity_binding_index != OpacityBindingIndex::INVALID {
                     let opacity_binding = &opacity_binding_store[opacity_binding_index];
                     for binding in &opacity_binding.bindings {
                         opacity_bindings.push(OpacityBinding::from(*binding));
                     }
                 }
 
                 image_keys.push(image_data.key);
-                true
+                false
             }
             PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
                 let yuv_image_data = &data_stores.yuv_image[data_handle].kind;
                 image_keys.extend_from_slice(&yuv_image_data.yuv_key);
-                true
+                false
+            }
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain => {
+                // Early exit to ensure this doesn't get added as a dependency on the tile.
+                return false;
             }
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::LineDecoration { .. } |
             PrimitiveInstanceKind::Clear { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } => {
                 // These don't contribute dependencies
-                true
+                false
             }
         };
 
-        // The transforms of any clips that are relative to the picture may affect
-        // the content rendered by this primitive.
-        let mut world_clip_rect = world_rect;
-        let mut culling_rect = prim_rect
-            .intersection(&prim_instance.local_clip_rect)
-            .unwrap_or_else(LayoutRect::zero);
-
-        // To maintain the previous logic, consider every clip in the current active
-        // clip stack that could affect this primitive.
-        // TODO(gw): We can make this much more efficient now, by taking advantage
-        //           of the per-picture clip chain information, rather then considering
-        //           it for every primitive, as we do here for simplicity.
-        for clip_stack in &clip_chain_stack.stack {
-            for clip_chain_id in clip_stack {
-                let mut current_clip_chain_id = *clip_chain_id;
-                while current_clip_chain_id != ClipChainId::NONE {
-                    let clip_chain_node = &clip_chain_nodes[current_clip_chain_id.0 as usize];
-                    let clip_node = &data_stores.clip[clip_chain_node.handle];
-
-                    // We can skip the root clip node - it will be taken care of by the
-                    // world bounding rect calculated for the cache.
-                    if current_clip_chain_id == self.root_clip_chain_id {
-                        current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
-                        continue;
-                    }
-
-                    self.map_local_to_world.set_target_spatial_node(
-                        clip_chain_node.spatial_node_index,
-                        clip_scroll_tree,
-                    );
-
-                    // Clips that are simple rects and handled by collapsing them into a single
-                    // clip rect. This avoids the need to store vertices for these cases, and also
-                    // allows easy calculation of the overall bounds of the tile cache.
-                    let add_to_clip_deps = match clip_node.item {
-                        ClipItem::Rectangle(size, ClipMode::Clip) => {
-                            let clip_spatial_node = &clip_scroll_tree.spatial_nodes[clip_chain_node.spatial_node_index.0 as usize];
-
-                            let local_clip_rect = LayoutRect::new(
-                                clip_chain_node.local_pos,
-                                size,
-                            );
-
-                            if clip_spatial_node.coordinate_system_id == CoordinateSystemId(0) {
-                                // Clips that are not in the root coordinate system are not axis-aligned,
-                                // so we need to treat them as normal style clips with vertices.
-                                match self.map_local_to_world.map(&local_clip_rect) {
-                                    Some(clip_world_rect) => {
-                                        // Even if this ends up getting clipped out by the current clip
-                                        // stack, we want to ensure the primitive gets added to the tiles
-                                        // below, to ensure invalidation isn't tripped up by the wrong
-                                        // number of primitives that affect this tile.
-                                        world_clip_rect = world_clip_rect
-                                            .intersection(&clip_world_rect)
-                                            .unwrap_or_else(WorldRect::zero);
-
-                                        // If the clip rect is in the same spatial node, it can be handled by the
-                                        // local clip rect.
-                                        if clip_chain_node.spatial_node_index == prim_instance.spatial_node_index {
-                                            culling_rect = culling_rect.intersection(&local_clip_rect).unwrap_or_else(LayoutRect::zero);
-
-                                            false
-                                        } else if !clip_scroll_tree.is_same_or_child_of(
-                                            clip_chain_node.spatial_node_index,
-                                            self.spatial_node_index,
-                                        ) {
-                                            // If the clip node is *not* a child of the main scroll root,
-                                            // add it to the list of potential world clips to be checked later.
-                                            // If it *is* a child of the main scroll root, then just track
-                                            // it as a normal clip dependency, since it likely moves in
-                                            // the same way as the primitive when scrolling (and if it doesn't,
-                                            // we want to invalidate and rasterize).
-                                            world_clips.push((
-                                                clip_world_rect.into(),
-                                                clip_chain_node.spatial_node_index,
-                                            ));
-
-                                            false
-                                        } else {
-                                            true
-                                        }
-                                    }
-                                    None => {
-                                        true
-                                    }
-                                }
-                            } else {
-                                true
-                            }
-                        }
-                        ClipItem::Rectangle(_, ClipMode::ClipOut) |
-                        ClipItem::RoundedRectangle(..) |
-                        ClipItem::Image { .. } |
-                        ClipItem::BoxShadow(..) => {
-                            true
-                        }
-                    };
-
-                    if add_to_clip_deps {
-                        clip_chain_uids.push(clip_chain_node.handle.uid());
-
-                        // If the clip has the same spatial node, the relative transform
-                        // will always be the same, so there's no need to depend on it.
-                        if clip_chain_node.spatial_node_index != self.spatial_node_index {
-                            clip_spatial_nodes.insert(clip_chain_node.spatial_node_index);
-                        }
-
-                        let local_clip_rect = LayoutRect::new(
-                            clip_chain_node.local_pos,
-                            LayoutSize::zero(),
-                        );
-                        if let Some(world_clip_rect) = self.map_local_to_world.map(&local_clip_rect) {
-                            clip_vertices.push(world_clip_rect.origin);
-                        }
-                    }
-
-                    current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+        // If there was a clip chain, add any clip dependencies to the list for this tile.
+        if let Some(prim_clip_chain) = prim_clip_chain {
+            prim_clip_rect = prim_clip_chain.pic_clip_rect;
+
+            let clip_instances = &clip_store
+                .clip_node_instances[prim_clip_chain.clips_range.to_range()];
+            for clip_instance in clip_instances {
+                clip_chain_uids.push(clip_instance.handle.uid());
+
+                // If the clip has the same spatial node, the relative transform
+                // will always be the same, so there's no need to depend on it.
+                if clip_instance.spatial_node_index != self.spatial_node_index {
+                    clip_spatial_nodes.insert(clip_instance.spatial_node_index);
                 }
+
+                clip_vertices.push(clip_instance.local_pos);
             }
         }
 
-        if include_clip_rect {
-            // Intersect the calculated prim bounds with the root clip rect, to save
-            // having to process and transform the root clip rect in every primitive.
-            if let Some(clipped_world_rect) = world_clip_rect.intersection(&self.root_clip_rect) {
-                self.world_bounding_rect = self.world_bounding_rect.union(&clipped_world_rect);
-            }
-        }
-
-        self.map_local_to_world.set_target_spatial_node(
-            prim_instance.spatial_node_index,
-            clip_scroll_tree,
-        );
-        let world_culling_rect = match self.map_local_to_world.map(&culling_rect) {
-            Some(rect) => rect,
-            None => return false,
-        };
-
         // Normalize the tile coordinates before adding to tile dependencies.
         // For each affected tile, mark any of the primitive dependencies.
         for y in p0.y .. p1.y {
             for x in p0.x .. p1.x {
-                let index = (y * self.tile_count.width + x) as usize;
-                let tile = &mut self.tiles[index];
-
-                // Store the local clip rect by calculating what portion
-                // of the tile it covers.
-                let world_culling_rect = world_culling_rect
-                    .intersection(&tile.world_rect)
-                    .map(|rect| {
-                        rect.translate(&-tile.world_rect.origin.to_vector())
-                    })
-                    .unwrap_or_else(WorldRect::zero)
-                    .round();
-
-                // Work out the needed rect for the primitive on this tile.
-                // TODO(gw): We should be able to remove this for any tile that is not
-                //           a partially clipped tile, which would be a significant
-                //           optimization for the common case (non-clipped tiles).
+                // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile?
+                let key = TileKey {
+                    slice: self.slice,
+                    offset: TileOffset::new(x, y),
+                };
+                let tile = self.tiles.get_mut(&key).expect("bug: no tile");
+
+                // Check if this tile becomes opaque due to this primitive.
+                if !tile.is_opaque {
+                    if let Some(ref opaque_rect) = opaque_rect {
+                        if opaque_rect.contains_rect(&tile.clipped_rect) {
+                            tile.is_opaque = true;
+                        }
+                    }
+                }
 
                 // Mark if the tile is cacheable at all.
                 tile.is_same_content &= is_cacheable;
 
                 // Include any image keys this tile depends on.
                 tile.descriptor.image_keys.extend_from_slice(&image_keys);
 
                 // // Include any opacity bindings this primitive depends on.
                 tile.descriptor.opacity_bindings.extend_from_slice(&opacity_bindings);
 
+                // TODO(gw): The origin of background rects produced by APZ changes
+                //           in Gecko during scrolling. Consider investigating this so the
+                //           hack / workaround below is not required.
+                let (prim_origin, prim_clip_rect) = if clip_by_tile {
+                    let tile_p0 = tile.clipped_rect.origin;
+                    let tile_p1 = tile.clipped_rect.bottom_right();
+
+                    let clip_p0 = PicturePoint::new(
+                        clampf(prim_clip_rect.origin.x, tile_p0.x, tile_p1.x),
+                        clampf(prim_clip_rect.origin.y, tile_p0.y, tile_p1.y),
+                    );
+
+                    let clip_p1 = PicturePoint::new(
+                        clampf(prim_clip_rect.origin.x + prim_clip_rect.size.width, tile_p0.x, tile_p1.x),
+                        clampf(prim_clip_rect.origin.y + prim_clip_rect.size.height, tile_p0.y, tile_p1.y),
+                    );
+
+                    (
+                        PicturePoint::new(
+                            clampf(prim_rect.origin.x, tile_p0.x, tile_p1.x),
+                            clampf(prim_rect.origin.y, tile_p0.y, tile_p1.y),
+                        ),
+                        PictureRect::new(
+                            clip_p0,
+                            PictureSize::new(
+                                clip_p1.x - clip_p0.x,
+                                clip_p1.y - clip_p0.y,
+                            ),
+                        ),
+                    )
+                } else {
+                    (prim_rect.origin, prim_clip_rect)
+                };
+
                 // Update the tile descriptor, used for tile comparison during scene swaps.
                 tile.descriptor.prims.push(PrimitiveDescriptor {
                     prim_uid: prim_instance.uid(),
-                    origin: (world_rect.origin - tile.world_rect.origin.to_vector()).round(),
+                    origin: prim_origin.into(),
                     first_clip: tile.descriptor.clip_uids.len() as u16,
                     clip_count: clip_chain_uids.len() as u16,
-                    world_culling_rect,
+                    prim_clip_rect: prim_clip_rect.into(),
                 });
+
                 tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
                 for clip_vertex in &clip_vertices {
-                    let clip_vertex = (*clip_vertex - tile.world_rect.origin.to_vector()).round();
-                    tile.descriptor.clip_vertices.push(clip_vertex.into());
+                    tile.descriptor.clip_vertices.push((*clip_vertex).into());
                 }
 
                 // If the primitive has the same spatial node, the relative transform
                 // will always be the same, so there's no need to depend on it.
                 if prim_instance.spatial_node_index != self.spatial_node_index {
                     tile.transforms.insert(prim_instance.spatial_node_index);
                 }
-
                 for spatial_node_index in &clip_spatial_nodes {
                     tile.transforms.insert(*spatial_node_index);
                 }
-                for (world_rect, spatial_node_index) in &world_clips {
-                    tile.potential_clips.insert(world_rect.clone(), *spatial_node_index);
-                }
             }
         }
 
         true
     }
 
     /// Apply any updates after prim dependency updates. This applies
     /// any late tile invalidations, and sets up the dirty rect and
     /// set of tile blits.
     pub fn post_update(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         frame_context: &FrameVisibilityContext,
         scratch: &mut PrimitiveScratchBuffer,
     ) {
+        self.tiles_to_draw.clear();
         self.dirty_region.clear();
-        self.pending_blits.clear();
-
-        // If the tile cache is disabled, just return a no-op local clip rect.
-        if !self.is_enabled {
-            return;
-        }
-
-        // Skip all tiles if completely off-screen.
-        if !self.world_bounding_rect.intersects(&frame_context.screen_world_rect) {
-            return;
-        }
-
-        let map_surface_to_world: SpaceMapper<LayoutPixel, WorldPixel> = SpaceMapper::new_with_target(
-            ROOT_SPATIAL_NODE_INDEX,
-            self.spatial_node_index,
-            frame_context.screen_world_rect,
-            frame_context.clip_scroll_tree,
-        );
-
-        self.local_clip_rect = map_surface_to_world
-            .unmap(&self.world_bounding_rect)
-            .expect("bug: unable to map local clip rect");
+        let mut dirty_region_index = 0;
+
+        // Track if all tiles are detected as opaque. Only when this occurs will
+        // we allow subpixel AA on this surface.
+        self.is_opaque = true;
 
         // Step through each tile and invalidate if the dependencies have changed.
-        for (i, tile) in self.tiles.iter_mut().enumerate() {
-            // Deal with any potential world clips. Check to see if they are
-            // outside the tile cache bounding rect. If they are, they're not
-            // relevant and we don't care if they move relative to the content
-            // itself. This avoids a lot of redundant invalidations.
-            for (clip_world_rect, spatial_node_index) in &tile.potential_clips {
-                let clip_world_rect = WorldRect::from(clip_world_rect.clone());
-                if !clip_world_rect.contains_rect(&self.world_bounding_rect) {
-                    tile.transforms.insert(*spatial_node_index);
-                }
-            }
+        for (key, tile) in self.tiles.iter_mut() {
+            self.is_opaque &= tile.is_opaque;
 
             // Update tile transforms
             let mut transform_spatial_nodes: Vec<SpatialNodeIndex> = tile.transforms.drain().collect();
             transform_spatial_nodes.sort();
             for spatial_node_index in transform_spatial_nodes {
                 // Note: this is the only place where we don't know beforehand if the tile-affecting
                 // spatial node is below or above the current picture.
-                let inverse_origin = if self.spatial_node_index >= spatial_node_index {
+                let transform = if self.spatial_node_index >= spatial_node_index {
                     frame_context.clip_scroll_tree
                         .get_relative_transform(
                             self.spatial_node_index,
                             spatial_node_index,
                         )
-                        .project_2d_origin()
                 } else {
                     frame_context.clip_scroll_tree
                         .get_relative_transform(
                             spatial_node_index,
                             self.spatial_node_index,
                         )
-                        .inverse_project_2d_origin()
                 };
-                // Store the result of transforming a fixed point by this
-                // transform.
-                // TODO(gw): This could in theory give incorrect results for a
-                //           primitive behind the near plane.
-                let key = inverse_origin
-                    .unwrap_or_else(LayoutPoint::zero)
-                    .round();
-                tile.descriptor.transforms.push(key.into());
+                tile.descriptor.transforms.push(transform.into());
             }
 
             // Invalidate if the backing texture was evicted.
             if resource_cache.texture_cache.is_allocated(&tile.handle) {
                 // Request the backing texture so it won't get evicted this frame.
                 // We specifically want to mark the tile texture as used, even
                 // if it's detected not visible below and skipped. This is because
                 // we maintain the set of tiles we care about based on visibility
                 // during pre_update. If a tile still exists after that, we are
                 // assuming that it's either visible or we want to retain it for
                 // a while in case it gets scrolled back onto screen soon.
                 // TODO(gw): Consider switching to manual eviction policy?
                 resource_cache.texture_cache.request(&tile.handle, gpu_cache);
             } else {
+                // When a tile is invalidated, reset the opacity information
+                // so that it is recalculated during prim dependency updates.
                 tile.is_valid = false;
             }
 
             // Invalidate the tile based on the content changing.
             tile.update_content_validity();
 
-            let visible_rect = match tile.visible_rect {
-                Some(rect) => rect,
-                None => continue,
-            };
-
-            // Check the valid rect of the primitive is sufficient.
-            let tile_bounding_rect = match visible_rect.intersection(&self.world_bounding_rect) {
-                Some(rect) => rect.translate(&-tile.world_rect.origin.to_vector()),
-                None => continue,
-            };
-
-            tile.update_rect_validity(&tile_bounding_rect);
-
             // If there are no primitives there is no need to draw or cache it.
             if tile.descriptor.prims.is_empty() {
                 continue;
             }
 
+            if !tile.world_rect.intersects(&frame_context.global_screen_world_rect) {
+                continue;
+            }
+
+            self.tiles_to_draw.push(*key);
+
             // Decide how to handle this tile when drawing this frame.
             if tile.is_valid {
-                // No need to include this is any dirty rect calculations.
-                tile.consider_for_dirty_rect = false;
-                self.tiles_to_draw.push(TileIndex(i));
-
                 if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
-                    if let Some(world_rect) = tile.world_rect.intersection(&self.world_bounding_rect) {
-                        let tile_device_rect = world_rect * frame_context.global_device_pixel_scale;
-                        let mut label_offset = DeviceVector2D::new(20.0, 30.0);
-                        scratch.push_debug_rect(
-                            tile_device_rect,
-                            debug_colors::GREEN,
+                    let tile_device_rect = tile.world_rect * frame_context.global_device_pixel_scale;
+                    let label_offset = DeviceVector2D::new(20.0, 30.0);
+                    let color = if tile.is_opaque {
+                        debug_colors::GREEN
+                    } else {
+                        debug_colors::YELLOW
+                    };
+                    scratch.push_debug_rect(
+                        tile_device_rect,
+                        color.scale_alpha(0.3),
+                    );
+                    if tile_device_rect.size.height >= label_offset.y {
+                        scratch.push_debug_string(
+                            tile_device_rect.origin + label_offset,
+                            debug_colors::RED,
+                            format!("{:?}: is_opaque={}", tile.id, tile.is_opaque),
                         );
-                        if tile_device_rect.size.height >= label_offset.y {
-                            scratch.push_debug_string(
-                                tile_device_rect.origin + label_offset,
-                                debug_colors::RED,
-                                format!("{:?} {:?} {:?}", tile.id, tile.handle, tile.world_rect),
-                            );
-                        }
-                        label_offset.y += 20.0;
-                        if tile_device_rect.size.height >= label_offset.y {
-                            scratch.push_debug_string(
-                                tile_device_rect.origin + label_offset,
-                                debug_colors::RED,
-                                format!("same: {} frames", tile.same_frames),
-                            );
-                        }
                     }
                 }
             } else {
                 if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
-                    if let Some(world_rect) = visible_rect.intersection(&self.world_bounding_rect) {
-                        scratch.push_debug_rect(
-                            world_rect * frame_context.global_device_pixel_scale,
-                            debug_colors::RED,
-                        );
-                    }
+                    scratch.push_debug_rect(
+                        tile.world_rect * frame_context.global_device_pixel_scale,
+                        debug_colors::RED,
+                    );
+                }
+
+                // Ensure that this texture is allocated.
+                if !resource_cache.texture_cache.is_allocated(&tile.handle) {
+                    resource_cache.texture_cache.update_picture_cache(
+                        &mut tile.handle,
+                        gpu_cache,
+                    );
                 }
 
-                // Only cache tiles that have had the same content for at least two
-                // frames. This skips caching on pages / benchmarks that are changing
-                // every frame, which is wasteful.
-                // When we are testing invalidation, we want WR to try to cache tiles each
-                // frame, to make it simpler to define the expected dirty rects.
-                if tile.same_frames >= FRAMES_BEFORE_PICTURE_CACHING || frame_context.config.testing {
-                    // Ensure that this texture is allocated.
-                    if !resource_cache.texture_cache.is_allocated(&tile.handle) {
-                        resource_cache.texture_cache.update_picture_cache(
-                            &mut tile.handle,
-                            gpu_cache,
-                        );
-                    }
-
-                    let cache_item = resource_cache
-                        .get_texture_cache_item(&tile.handle);
-
-                    let src_origin = (visible_rect.origin * frame_context.global_device_pixel_scale).round().to_i32();
-                    let valid_rect = visible_rect.translate(&-tile.world_rect.origin.to_vector());
-
-                    tile.valid_rect = visible_rect
-                        .intersection(&self.world_bounding_rect)
-                        .map(|rect| rect.translate(&-tile.world_rect.origin.to_vector()))
-                        .unwrap_or_else(WorldRect::zero);
-
-                    // Store a blit operation to be done after drawing the
-                    // frame in order to update the cached texture tile.
-                    let dest_rect = (valid_rect * frame_context.global_device_pixel_scale).round().to_i32();
-                    self.pending_blits.push(TileBlit {
-                        target: cache_item,
-                        src_offset: src_origin,
-                        dest_offset: dest_rect.origin,
-                        size: dest_rect.size,
-                    });
-
-                    // We can consider this tile valid now.
-                    tile.is_valid = true;
+                tile.visibility_mask = PrimitiveVisibilityMask::empty();
+
+                // If we run out of dirty regions, then force the last dirty region to
+                // be a union of any remaining regions. This is an inefficiency, in that
+                // we'll add items to batches later on that are redundant / outside this
+                // tile, but it's really rare except in pathological cases (even on a
+                // 4k screen, the typical dirty region count is < 16).
+                if dirty_region_index < PrimitiveVisibilityMask::MAX_DIRTY_REGIONS {
+                    tile.visibility_mask.set_visible(dirty_region_index);
+
+                    self.dirty_region.push(
+                        tile.world_rect,
+                        tile.visibility_mask,
+                    );
+
+                    dirty_region_index += 1;
+                } else {
+                    tile.visibility_mask.set_visible(PrimitiveVisibilityMask::MAX_DIRTY_REGIONS - 1);
+
+                    self.dirty_region.include_rect(
+                        PrimitiveVisibilityMask::MAX_DIRTY_REGIONS - 1,
+                        tile.world_rect,
+                    );
                 }
-
-                // This tile should be considered as part of the dirty rect calculations.
-                tile.consider_for_dirty_rect = true;
             }
         }
 
-        // Build a minimal set of dirty rects from the set of dirty tiles that
-        // were found above.
-        let mut builder = DirtyRegionBuilder::new(
-            &mut self.tiles,
-            self.tile_count,
-        );
-
-        builder.build(&mut self.dirty_region);
-
         // When under test, record a copy of the dirty region to support
         // invalidation testing in wrench.
         if frame_context.config.testing {
             scratch.recorded_dirty_regions.push(self.dirty_region.record());
         }
-
-        // If we end up with too many dirty rects, then it's going to be a lot
-        // of extra draw calls to submit (since we currently just submit every
-        // draw call for every dirty rect). In this case, bail out and work
-        // with a single, large dirty rect. In future we can consider improving
-        // on this by supporting batching per dirty region.
-        if self.dirty_region.dirty_rects.len() > MAX_DIRTY_RECTS {
-            self.dirty_region.collapse();
-        }
-    }
-
-    pub fn tile_dimensions(testing: bool) -> DeviceIntSize {
-        if testing {
-            size2(TILE_SIZE_TESTING, TILE_SIZE_TESTING)
-        } else {
-            size2(TILE_SIZE_WIDTH, TILE_SIZE_HEIGHT)
-        }
     }
 }
 
 /// Maintains a stack of picture and surface information, that
 /// is used during the initial picture traversal.
 pub struct PictureUpdateState<'a> {
     surfaces: &'a mut Vec<SurfaceInfo>,
     surface_stack: Vec<SurfaceIndex>,
@@ -1801,29 +1440,26 @@ pub struct SurfaceInfo {
     pub raster_spatial_node_index: SpatialNodeIndex,
     pub surface_spatial_node_index: SpatialNodeIndex,
     /// This is set when the render task is created.
     pub render_tasks: Option<SurfaceRenderTasks>,
     /// How much the local surface rect should be inflated (for blur radii).
     pub inflation_factor: f32,
     /// The device pixel ratio specific to this surface.
     pub device_pixel_scale: DevicePixelScale,
-    /// If true, subpixel AA rendering can be used on this surface.
-    pub allow_subpixel_aa: bool,
 }
 
 impl SurfaceInfo {
     pub fn new(
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         inflation_factor: f32,
         world_rect: WorldRect,
         clip_scroll_tree: &ClipScrollTree,
         device_pixel_scale: DevicePixelScale,
-        allow_subpixel_aa: bool,
     ) -> Self {
         let map_surface_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
             world_rect,
             clip_scroll_tree,
         );
 
@@ -1839,17 +1475,16 @@ impl SurfaceInfo {
         SurfaceInfo {
             rect: PictureRect::zero(),
             map_local_to_surface,
             render_tasks: None,
             raster_spatial_node_index,
             surface_spatial_node_index,
             inflation_factor,
             device_pixel_scale,
-            allow_subpixel_aa,
         }
     }
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct RasterConfig {
     /// How this picture should be composited into
@@ -1887,17 +1522,16 @@ pub enum PictureCompositeMode {
     Filter(Filter),
     /// Apply a component transfer filter.
     ComponentTransferFilter(FilterDataHandle),
     /// Draw to intermediate surface, copy straight across. This
     /// is used for CSS isolation, and plane splitting.
     Blit(BlitReason),
     /// Used to cache a picture as a series of tiles.
     TileCache {
-        clear_color: ColorF,
     },
 }
 
 /// Enum value describing the place of a picture in a 3D context.
 #[derive(Clone, Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub enum Picture3DContext<C> {
     /// The picture is not a part of 3D context sub-hierarchy.
@@ -2037,78 +1671,92 @@ impl PrimitiveList {
                     pictures.push(pic_index);
                     true
                 }
                 _ => {
                     false
                 }
             };
 
-            let prim_data = match prim_instance.kind {
+            let (is_backface_visible, prim_size) = match prim_instance.kind {
                 PrimitiveInstanceKind::Rectangle { data_handle, .. } |
                 PrimitiveInstanceKind::Clear { data_handle, .. } => {
-                    &interners.prim[data_handle]
+                    let data = &interners.prim[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::Image { data_handle, .. } => {
-                    &interners.image[data_handle]
+                    let data = &interners.image[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
-                    &interners.image_border[data_handle]
+                    let data = &interners.image_border[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::LineDecoration { data_handle, .. } => {
-                    &interners.line_decoration[data_handle]
+                    let data = &interners.line_decoration[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
-                    &interners.linear_grad[data_handle]
+                    let data = &interners.linear_grad[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
-                    &interners.normal_border[data_handle]
+                    let data = &interners.normal_border[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::Picture { data_handle, .. } => {
-                    &interners.picture[data_handle]
+                    let data = &interners.picture[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::RadialGradient { data_handle, ..} => {
-                    &interners.radial_grad[data_handle]
+                    let data = &interners.radial_grad[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::TextRun { data_handle, .. } => {
-                    &interners.text_run[data_handle]
+                    let data = &interners.text_run[data_handle];
+                    (data.is_backface_visible, data.prim_size)
                 }
                 PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
-                    &interners.yuv_image[data_handle]
+                    let data = &interners.yuv_image[data_handle];
+                    (data.is_backface_visible, data.prim_size)
+                }
+                PrimitiveInstanceKind::PushClipChain |
+                PrimitiveInstanceKind::PopClipChain => {
+                    (true, LayoutSize::zero())
                 }
             };
 
             // Get the key for the cluster that this primitive should
             // belong to.
             let key = PrimitiveClusterKey {
                 spatial_node_index: prim_instance.spatial_node_index,
-                is_backface_visible: prim_data.is_backface_visible,
+                is_backface_visible,
             };
 
             // Find the cluster, or create a new one.
             let cluster_index = *clusters_map
                 .entry(key)
                 .or_insert_with(|| {
                     let index = clusters.len();
                     clusters.push(PrimitiveCluster::new(
                         prim_instance.spatial_node_index,
-                        prim_data.is_backface_visible,
+                        is_backface_visible,
                     ));
                     index
                 }
             );
 
             // Pictures don't have a known static local bounding rect (they are
             // calculated during the picture traversal dynamically). If not
             // a picture, include a minimal bounding rect in the cluster bounds.
             let cluster = &mut clusters[cluster_index];
             if !is_pic {
                 let prim_rect = LayoutRect::new(
                     prim_instance.prim_origin,
-                    prim_data.prim_size,
+                    prim_size,
                 );
                 let culling_rect = prim_instance.local_clip_rect
                     .intersection(&prim_rect)
                     .unwrap_or_else(LayoutRect::zero);
 
                 cluster.bounding_rect = cluster.bounding_rect.union(&culling_rect);
             }
 
@@ -2142,19 +1790,16 @@ impl Default for PictureOptions {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct PicturePrimitive {
     /// List of primitives, and associated info for this picture.
     pub prim_list: PrimitiveList,
 
     #[cfg_attr(feature = "capture", serde(skip))]
     pub state: Option<PictureState>,
 
-    /// The pipeline that the primitives on this picture belong to.
-    pub pipeline_id: PipelineId,
-
     /// If true, apply the local clip rect to primitive drawn
     /// in this picture.
     pub apply_local_clip_rect: bool,
     /// If false and transform ends up showing the back of the picture,
     /// it will be considered invisible.
     pub is_backface_visible: bool,
 
     // If a mix-blend-mode, contains the render task for
@@ -2202,17 +1847,17 @@ pub struct PicturePrimitive {
     /// If false, this picture needs to (re)build segments
     /// if it supports segment rendering. This can occur
     /// if the local rect of the picture changes due to
     /// transform animation and/or scrolling.
     pub segments_are_valid: bool,
 
     /// If Some(..) the tile cache that is associated with this picture.
     #[cfg_attr(feature = "capture", serde(skip))] //TODO
-    pub tile_cache: Option<Box<TileCache>>,
+    pub tile_cache: Option<Box<TileCacheInstance>>,
 
     /// The config options for this picture.
     options: PictureOptions,
 }
 
 impl PicturePrimitive {
     pub fn print<T: PrintTreePrinter>(
         &self,
@@ -2281,60 +1926,49 @@ impl PicturePrimitive {
 
     /// Destroy an existing picture. This is called just before
     /// a frame builder is replaced with a newly built scene. It
     /// gives a picture a chance to retain any cached tiles that
     /// may be useful during the next scene build.
     pub fn destroy(
         &mut self,
         retained_tiles: &mut RetainedTiles,
-        clip_scroll_tree: &ClipScrollTree,
     ) {
         if let Some(tile_cache) = self.tile_cache.take() {
-            // Calculate and store positions of the reference
-            // primitives for this tile cache.
-            build_ref_prims(
-                &tile_cache.reference_prims.ref_prims,
-                &mut retained_tiles.ref_prims,
-                clip_scroll_tree,
-            );
-
             retained_tiles.tiles.extend(tile_cache.tiles);
         }
     }
 
     // TODO(gw): We have the PictureOptions struct available. We
     //           should move some of the parameter list in this
     //           method to be part of the PictureOptions, and
     //           avoid adding new parameters here.
     pub fn new_image(
         requested_composite_mode: Option<PictureCompositeMode>,
         context_3d: Picture3DContext<OrderedPictureChild>,
-        pipeline_id: PipelineId,
         frame_output_pipeline_id: Option<PipelineId>,
         apply_local_clip_rect: bool,
         is_backface_visible: bool,
         requested_raster_space: RasterSpace,
         prim_list: PrimitiveList,
         spatial_node_index: SpatialNodeIndex,
-        tile_cache: Option<Box<TileCache>>,
+        tile_cache: Option<Box<TileCacheInstance>>,
         options: PictureOptions,
     ) -> Self {
         PicturePrimitive {
             prim_list,
             state: None,
             secondary_render_task_id: None,
             requested_composite_mode,
             raster_config: None,
             context_3d,
             frame_output_pipeline_id,
             extra_gpu_data_handles: SmallVec::new(),
             apply_local_clip_rect,
             is_backface_visible,
-            pipeline_id,
             requested_raster_space,
             spatial_node_index,
             snapped_local_rect: LayoutRect::zero(),
             unsnapped_local_rect: LayoutRect::zero(),
             tile_cache,
             options,
             segments_are_valid: false,
         }
@@ -2364,16 +1998,17 @@ impl PicturePrimitive {
 
     pub fn take_context(
         &mut self,
         pic_index: PictureIndex,
         clipped_prim_bounding_rect: WorldRect,
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         parent_surface_index: SurfaceIndex,
+        parent_subpixel_mode: SubpixelMode,
         frame_state: &mut FrameBuildingState,
         frame_context: &FrameBuildingContext,
     ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
         if !self.is_visible() {
             return None;
         }
 
         // Extract the raster and surface spatial nodes from the raster
@@ -2398,66 +2033,48 @@ impl PicturePrimitive {
                     0.0,
                 )
             }
         };
 
         let map_pic_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
-            frame_context.screen_world_rect,
+            frame_context.global_screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or_else(PictureRect::max_rect);
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
-            frame_context.screen_world_rect,
+            frame_context.global_screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let plane_splitter = match self.context_3d {
             Picture3DContext::Out => {
                 None
             }
             Picture3DContext::In { root_data: Some(_), .. } => {
                 Some(PlaneSplitter::new())
             }
             Picture3DContext::In { root_data: None, .. } => {
                 None
             }
         };
 
-        let (is_composite, is_passthrough) = match self.raster_config {
-            Some(ref rc @ RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) => {
-                // For a picture surface, just push any child tasks and tile
-                // blits up to the parent surface.
-                let port = frame_state
-                    .surfaces[parent_surface_index.0]
-                    .render_tasks
-                    .expect("bug: no render tasks set for parent!")
-                    .port;
-
-                frame_state
-                    .surfaces[rc.surface_index.0]
-                    .render_tasks = Some(SurfaceRenderTasks {
-                        root: RenderTaskId::INVALID,
-                        port,
-                    });
-
-                (false, false)
-            },
+        match self.raster_config {
             Some(ref raster_config) => {
                 let pic_rect = PictureRect::from_untyped(&self.snapped_local_rect.to_untyped());
 
                 let device_pixel_scale = frame_state
                     .surfaces[raster_config.surface_index.0]
                     .device_pixel_scale;
 
                 let (clipped, unclipped) = match get_raster_rects(
@@ -2469,20 +2086,17 @@ impl PicturePrimitive {
                 ) {
                     Some(info) => info,
                     None => {
                         return None
                     }
                 };
                 let transform = map_pic_to_raster.get_transform();
 
-                let (root, port) = match raster_config.composite_mode {
-                    PictureCompositeMode::TileCache { .. } => {
-                        unreachable!();
-                    }
+                let dep_info = match raster_config.composite_mode {
                     PictureCompositeMode::Filter(Filter::Blur(blur_radius)) => {
                         let blur_std_deviation = blur_radius * device_pixel_scale.0;
                         let scale_factors = scale_factors(&transform);
                         let blur_std_deviation = DeviceSize::new(
                             blur_std_deviation * scale_factors.0,
                             blur_std_deviation * scale_factors.1
                         );
                         let inflation_factor = frame_state.surfaces[raster_config.surface_index.0].inflation_factor;
@@ -2517,33 +2131,33 @@ impl PicturePrimitive {
                         );
 
                         let picture_task = RenderTask::new_picture(
                             RenderTaskLocation::Dynamic(None, device_rect.size),
                             unclipped.size,
                             pic_index,
                             device_rect.origin,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
 
                         let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                         let blur_render_task_id = RenderTask::new_blur(
                             blur_std_deviation,
                             picture_task_id,
                             frame_state.render_tasks,
                             RenderTargetKind::Color,
                             ClearMode::Transparent,
                             None,
                         );
 
-                        (blur_render_task_id, picture_task_id)
+                        Some((blur_render_task_id, picture_task_id))
                     }
                     PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
                         let mut max_std_deviation = 0.0;
                         for shadow in shadows {
                             // TODO(nical) presumably we should compute the clipped rect for each shadow
                             // and compute the union of them to determine what we need to rasterize and blur?
                             max_std_deviation = f32::max(max_std_deviation, shadow.blur_radius * device_pixel_scale.0);
                         }
@@ -2567,19 +2181,19 @@ impl PicturePrimitive {
                         );
 
                         let mut picture_task = RenderTask::new_picture(
                             RenderTaskLocation::Dynamic(None, device_rect.size),
                             unclipped.size,
                             pic_index,
                             device_rect.origin,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
                         picture_task.mark_for_saving();
 
                         let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                         self.secondary_render_task_id = Some(picture_task_id);
 
                         let mut blur_tasks = BlurTaskCache::default();
@@ -2595,100 +2209,161 @@ impl PicturePrimitive {
                                 frame_state.render_tasks,
                                 RenderTargetKind::Color,
                                 ClearMode::Transparent,
                                 Some(&mut blur_tasks),
                             );
                         }
 
                         // TODO(nical) the second one should to be the blur's task id but we have several blurs now
-                        (blur_render_task_id, picture_task_id)
+                        Some((blur_render_task_id, picture_task_id))
                     }
                     PictureCompositeMode::MixBlend(..) if !frame_context.fb_config.gpu_supports_advanced_blend => {
                         let uv_rect_kind = calculate_uv_rect_kind(
                             &pic_rect,
                             &transform,
                             &clipped,
                             device_pixel_scale,
                             true,
                         );
 
                         let picture_task = RenderTask::new_picture(
                             RenderTaskLocation::Dynamic(None, clipped.size),
                             unclipped.size,
                             pic_index,
                             clipped.origin,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
 
                         let readback_task_id = frame_state.render_tasks.add(
                             RenderTask::new_readback(clipped)
                         );
 
                         frame_state.render_tasks.add_dependency(
                             frame_state.surfaces[parent_surface_index.0].render_tasks.unwrap().port,
                             readback_task_id,
                         );
 
                         self.secondary_render_task_id = Some(readback_task_id);
 
                         let render_task_id = frame_state.render_tasks.add(picture_task);
 
-                        (render_task_id, render_task_id)
+                        Some((render_task_id, render_task_id))
                     }
                     PictureCompositeMode::Filter(..) => {
                         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,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
 
                         let render_task_id = frame_state.render_tasks.add(picture_task);
 
-                        (render_task_id, render_task_id)
+                        Some((render_task_id, render_task_id))
                     }
                     PictureCompositeMode::ComponentTransferFilter(..) => {
                         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,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
 
                         let render_task_id = frame_state.render_tasks.add(picture_task);
 
-                        (render_task_id, render_task_id)
+                        Some((render_task_id, render_task_id))
+                    }
+                    PictureCompositeMode::TileCache { .. } => {
+                        let tile_cache = self.tile_cache.as_mut().unwrap();
+                        let mut first = true;
+
+                        let tile_size = DeviceSize::new(
+                            TILE_SIZE_WIDTH as f32,
+                            TILE_SIZE_HEIGHT as f32,
+                        );
+
+                        for key in &tile_cache.tiles_to_draw {
+                            let tile = tile_cache.tiles.get_mut(key).expect("bug: no tile found!");
+
+                            if tile.is_valid {
+                                continue;
+                            }
+
+                            // TODO(gw): Is this ever fractional? Can that cause seams?
+                            let content_origin = (tile.world_rect.origin * device_pixel_scale)
+                                .round().to_i32();
+
+                            let cache_item = frame_state.resource_cache.texture_cache.get(&tile.handle);
+
+                            let task = RenderTask::new_picture(
+                                RenderTaskLocation::PictureCache {
+                                    texture: cache_item.texture_id,
+                                    layer: cache_item.texture_layer,
+                                    size: tile_size.to_i32(),
+                                },
+                                tile_size,
+                                pic_index,
+                                content_origin,
+                                UvRectKind::Rect,
+                                surface_spatial_node_index,
+                                device_pixel_scale,
+                                tile.visibility_mask,
+                            );
+
+                            let render_task_id = frame_state.render_tasks.add(task);
+
+                            frame_state.render_tasks.add_dependency(
+                                frame_state.surfaces[parent_surface_index.0].render_tasks.unwrap().port,
+                                render_task_id,
+                            );
+
+                            if first {
+                                // TODO(gw): Maybe we can restructure this code to avoid the
+                                //           first hack here. Or at least explain it with a follow up
+                                //           bug.
+                                frame_state.surfaces[raster_config.surface_index.0].render_tasks = Some(SurfaceRenderTasks {
+                                    root: render_task_id,
+                                    port: render_task_id,
+                                });
+
+                                first = false;
+                            }
+
+                            tile.is_valid = true;
+                        }
+
+                        None
                     }
                     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,
                             _ => true,
@@ -2703,81 +2378,109 @@ impl PicturePrimitive {
                         );
 
                         let picture_task = RenderTask::new_picture(
                             RenderTaskLocation::Dynamic(None, clipped.size),
                             unclipped.size,
                             pic_index,
                             clipped.origin,
                             uv_rect_kind,
-                            raster_spatial_node_index,
                             surface_spatial_node_index,
                             device_pixel_scale,
+                            PrimitiveVisibilityMask::all(),
                         );
 
                         let render_task_id = frame_state.render_tasks.add(picture_task);
 
-                        (render_task_id, render_task_id)
+                        Some((render_task_id, render_task_id))
                     }
                 };
 
-                frame_state.surfaces[raster_config.surface_index.0].render_tasks = Some(SurfaceRenderTasks {
-                    root,
-                    port,
-                });
-
-                frame_state.render_tasks.add_dependency(
-                    frame_state.surfaces[parent_surface_index.0].render_tasks.unwrap().port,
-                    root,
-                );
-
-                (true, false)
+                if let Some((root, port)) = dep_info {
+                    frame_state.surfaces[raster_config.surface_index.0].render_tasks = Some(SurfaceRenderTasks {
+                        root,
+                        port,
+                    });
+
+                    frame_state.render_tasks.add_dependency(
+                        frame_state.surfaces[parent_surface_index.0].render_tasks.unwrap().port,
+                        root,
+                    );
+                }
             }
-            None => {
-                (false, true)
-            }
+            None => {}
         };
 
         let state = PictureState {
             //TODO: check for MAX_CACHE_SIZE here?
             map_local_to_pic,
             map_pic_to_world,
             map_pic_to_raster,
             map_raster_to_world,
             plane_splitter,
         };
 
         let mut dirty_region_count = 0;
 
         // If this is a picture cache, push the dirty region to ensure any
         // child primitives are culled and clipped to the dirty rect(s).
-        if let Some(ref tile_cache) = self.tile_cache {
-            // If the tile cache is disabled, it doesn't have a valid
-            // dirty region to exclude primitives from.
-            if tile_cache.is_enabled {
-                frame_state.push_dirty_region(tile_cache.dirty_region.clone());
-                dirty_region_count += 1;
-            }
+        if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) = self.raster_config {
+            let dirty_region = self.tile_cache.as_ref().unwrap().dirty_region.clone();
+            frame_state.push_dirty_region(dirty_region);
+            dirty_region_count += 1;
         }
 
         if inflation_factor > 0.0 {
             let inflated_region = frame_state.current_dirty_region().inflate(inflation_factor);
             frame_state.push_dirty_region(inflated_region);
             dirty_region_count += 1;
         }
 
+        // Disallow subpixel AA if an intermediate surface is needed.
+        // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
+        let (is_passthrough, subpixel_mode) = match self.raster_config {
+            Some(RasterConfig { ref composite_mode, .. }) => {
+                let subpixel_mode = match composite_mode {
+                    PictureCompositeMode::TileCache { .. } => {
+                        // TODO(gw): Maintaining old behaviour, we know that any slice
+                        //           that was created is allowed to have subpixel text on it.
+                        //           Once we enable multiple slices, we'll need to handle this
+                        //           condition properly.
+                        SubpixelMode::Allow
+                    }
+                    PictureCompositeMode::Blit(..) |
+                    PictureCompositeMode::ComponentTransferFilter(..) |
+                    PictureCompositeMode::Filter(..) |
+                    PictureCompositeMode::MixBlend(..) => {
+                        SubpixelMode::Deny
+                    }
+                };
+
+                (false, subpixel_mode)
+            }
+            None => {
+                (true, SubpixelMode::Allow)
+            }
+        };
+
+        // Still disable subpixel AA if parent forbids it
+        let subpixel_mode = match (parent_subpixel_mode, subpixel_mode) {
+            (SubpixelMode::Allow, SubpixelMode::Allow) => SubpixelMode::Allow,
+            _ => SubpixelMode::Deny,
+        };
+
         let context = PictureContext {
             pic_index,
             apply_local_clip_rect: self.apply_local_clip_rect,
-            is_composite,
             is_passthrough,
             raster_spatial_node_index,
             surface_spatial_node_index,
             surface_index,
             dirty_region_count,
+            subpixel_mode,
         };
 
         let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
 
         Some((context, state, prim_list))
     }
 
     pub fn restore_context(
@@ -2949,25 +2652,35 @@ impl PicturePrimitive {
         // Push information about this pic on stack for children to read.
         state.push_picture(PictureInfo {
             _spatial_node_index: self.spatial_node_index,
         });
 
         // See if this picture actually needs a surface for compositing.
         let actual_composite_mode = match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(ref filter)) if filter.is_noop() => None,
+            Some(PictureCompositeMode::TileCache { .. }) => {
+                // Disable tile cache if the scroll root has a perspective transform, since
+                // this breaks many assumptions (it's a very rare edge case anyway, and
+                // is probably (?) going to be moving / animated in this case).
+                let spatial_node = &frame_context
+                    .clip_scroll_tree
+                    .spatial_nodes[self.spatial_node_index.0 as usize];
+                if spatial_node.coordinate_system_id == CoordinateSystemId::root() {
+                    Some(PictureCompositeMode::TileCache { })
+                } else {
+                    None
+                }
+            },
             ref mode => mode.clone(),
         };
 
         if let Some(composite_mode) = actual_composite_mode {
             // Retrieve the positioning node information for the parent surface.
-            let (parent_raster_node_index, parent_allows_subpixel_aa)= {
-                let parent_surface = state.current_surface();
-                (parent_surface.raster_spatial_node_index, parent_surface.allow_subpixel_aa)
-            };
+            let parent_raster_node_index = state.current_surface().raster_spatial_node_index;
             let surface_spatial_node_index = self.spatial_node_index;
 
             // This inflation factor is to be applied to all primitives within the surface.
             let inflation_factor = match composite_mode {
                 PictureCompositeMode::Filter(Filter::Blur(blur_radius)) => {
                     // Only inflate if the caller hasn't already inflated
                     // the bounding rects for this filter.
                     if self.options.inflate_if_required {
@@ -2984,46 +2697,27 @@ impl PicturePrimitive {
             };
 
             // Check if there is perspective, and thus whether a new
             // rasterization root should be established.
             let establishes_raster_root = frame_context.clip_scroll_tree
                 .get_relative_transform(surface_spatial_node_index, parent_raster_node_index)
                 .is_perspective();
 
-            // Disallow subpixel AA if an intermediate surface is needed.
-            // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
-            let allow_subpixel_aa = match composite_mode {
-                PictureCompositeMode::TileCache { clear_color, .. } => {
-                    // If the tile cache has an opaque background, then it's fine to use
-                    // subpixel rendering (this is the common case).
-                    clear_color.a >= 1.0
-                }
-                PictureCompositeMode::Blit(..) |
-                PictureCompositeMode::ComponentTransferFilter(..) |
-                PictureCompositeMode::Filter(..) |
-                PictureCompositeMode::MixBlend(..) => {
-                    false
-                }
-            };
-            // Still disable subpixel AA if parent forbids it
-            let allow_subpixel_aa = parent_allows_subpixel_aa && allow_subpixel_aa;
-
             let surface = SurfaceInfo::new(
                 surface_spatial_node_index,
                 if establishes_raster_root {
                     surface_spatial_node_index
                 } else {
                     parent_raster_node_index
                 },
                 inflation_factor,
-                frame_context.screen_world_rect,
+                frame_context.global_screen_world_rect,
                 &frame_context.clip_scroll_tree,
                 frame_context.global_device_pixel_scale,
-                allow_subpixel_aa,
             );
 
             self.raster_config = Some(RasterConfig {
                 composite_mode,
                 establishes_raster_root,
                 surface_index: state.push_surface(surface),
             });
         }
@@ -3359,81 +3053,8 @@ fn create_raster_mappers(
         raster_spatial_node_index,
         surface_spatial_node_index,
         raster_bounds,
         clip_scroll_tree,
     );
 
     (map_raster_to_world, map_pic_to_raster)
 }
-
-// Convert a list of reference primitives into a map of prim uid -> world position.
-fn build_ref_prims(
-    ref_prims: &[ReferencePrimitive],
-    prim_map: &mut FastHashMap<ItemUid, WorldPoint>,
-    clip_scroll_tree: &ClipScrollTree,
-) {
-    prim_map.clear();
-
-    let mut map_local_to_world = SpaceMapper::new(
-        ROOT_SPATIAL_NODE_INDEX,
-        WorldRect::zero(),
-    );
-
-    for ref_prim in ref_prims {
-        map_local_to_world.set_target_spatial_node(
-            ref_prim.spatial_node_index,
-            clip_scroll_tree,
-        );
-
-        // We only care about the origin.
-        // TODO(gw): Consider adding a map_point to SpaceMapper.
-        let rect = LayoutRect::new(
-            ref_prim.local_pos,
-            LayoutSize::zero(),
-        );
-
-        if let Some(rect) = map_local_to_world.map(&rect) {
-            prim_map.insert(ref_prim.uid, rect.origin);
-        }
-    }
-}
-
-// Attempt to correlate the offset between two display lists by
-// comparing the offsets between a small number of primitives in
-// each display list.
-// TODO(gw): This is basically a horrible hack - there must be a better
-//           way to achieve this!
-fn correlate_prim_maps(
-    old_prims: &FastHashMap<ItemUid, WorldPoint>,
-    new_prims: &FastHashMap<ItemUid, WorldPoint>,
-) -> Option<WorldVector2D> {
-    let mut map: FastHashMap<VectorKey, usize> = FastHashMap::default();
-
-    // Find primitives with the same uid, find the difference
-    // between them and store the frequency of this offset
-    // in a hash map.
-    for (uid, old_point) in old_prims {
-        if let Some(new_point) = new_prims.get(uid) {
-            let key = (*new_point - *old_point).round().into();
-
-            let key_count = map.entry(key).or_insert(0);
-            *key_count += 1;
-        }
-    }
-
-    // Calculate the mode (the most common frequency of offset). This
-    // can be different for some primitives, if they've animated, or
-    // are attached to a different scroll node etc.
-    map.into_iter()
-        .max_by_key(|&(_, count)| count)
-        .and_then(|(offset, count)| {
-            // We will assume we can use the calculated offset if we
-            // found more than one quarter of the selected reference
-            // primitives to have the same offset.
-            let prims_available = new_prims.len().min(old_prims.len());
-            if count >= prims_available / 4 {
-                Some(offset.into())
-            } else {
-                None
-            }
-        })
-}
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{BorderRadius, ClipMode, ColorF};
 use api::{ImageRendering, RepeatMode};
 use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
 use api::{BoxShadowClipMode, LineStyle, LineOrientation};
-use api::{PrimitiveKeyKind, RasterSpace};
+use api::{PrimitiveKeyKind};
 use api::units::*;
 use crate::border::{get_max_scale_for_border, build_border_instances};
 use crate::border::BorderSegmentCacheKey;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
 use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem};
 use crate::debug_colors;
@@ -22,18 +22,18 @@ use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::glyph_rasterizer::GlyphKey;
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks};
 use crate::gpu_types::{BrushFlags, SnapOffsets};
 use crate::image::{Repetition};
 use crate::intern;
 use malloc_size_of::MallocSizeOf;
-use crate::picture::{PictureCompositeMode, PicturePrimitive, SurfaceInfo};
-use crate::picture::{ClusterIndex, PrimitiveList, RecordedDirtyRegion, SurfaceIndex, RetainedTiles};
+use crate::picture::{PictureCompositeMode, PicturePrimitive};
+use crate::picture::{ClusterIndex, PrimitiveList, RecordedDirtyRegion, SurfaceIndex, RetainedTiles, RasterConfig};
 use crate::prim_store::borders::{ImageBorderDataHandle, NormalBorderDataHandle};
 use crate::prim_store::gradient::{GRADIENT_FP_STOPS, GradientCacheKey, GradientStopKey};
 use crate::prim_store::gradient::{LinearGradientPrimitive, LinearGradientDataHandle, RadialGradientDataHandle};
 use crate::prim_store::image::{ImageDataHandle, ImageInstance, VisibleImageTile, YuvImageDataHandle};
 use crate::prim_store::line_dec::LineDecorationDataHandle;
 use crate::prim_store::picture::PictureDataHandle;
 use crate::prim_store::text_run::{TextRunDataHandle, TextRunPrimitive};
 #[cfg(debug_assertions)]
@@ -304,17 +304,17 @@ impl GpuCacheAddress {
 #[derive(MallocSizeOf)]
 pub struct PrimitiveSceneData {
     pub prim_size: LayoutSize,
     pub is_backface_visible: bool,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
 pub struct RectangleKey {
     x: f32,
     y: f32,
     w: f32,
     h: f32,
 }
 
 impl Eq for RectangleKey {}
@@ -352,16 +352,27 @@ impl From<LayoutRect> for RectangleKey {
             x: rect.origin.x,
             y: rect.origin.y,
             w: rect.size.width,
             h: rect.size.height,
         }
     }
 }
 
+impl From<PictureRect> for RectangleKey {
+    fn from(rect: PictureRect) -> RectangleKey {
+        RectangleKey {
+            x: rect.origin.x,
+            y: rect.origin.y,
+            w: rect.size.width,
+            h: rect.size.height,
+        }
+    }
+}
+
 impl From<WorldRect> for RectangleKey {
     fn from(rect: WorldRect) -> RectangleKey {
         RectangleKey {
             x: rect.origin.x,
             y: rect.origin.y,
             w: rect.size.width,
             h: rect.size.height,
         }
@@ -532,16 +543,25 @@ impl From<LayoutPoint> for PointKey {
     fn from(p: LayoutPoint) -> PointKey {
         PointKey {
             x: p.x,
             y: p.y,
         }
     }
 }
 
+impl From<PicturePoint> for PointKey {
+    fn from(p: PicturePoint) -> PointKey {
+        PointKey {
+            x: p.x,
+            y: p.y,
+        }
+    }
+}
+
 impl From<WorldPoint> for PointKey {
     fn from(p: WorldPoint) -> PointKey {
         PointKey {
             x: p.x,
             y: p.y,
         }
     }
 }
@@ -958,20 +978,22 @@ impl BrushSegment {
                     frame_state.render_tasks,
                     clip_data_store,
                     snap_offsets,
                     device_pixel_scale,
                     frame_context.fb_config,
                 );
 
                 let clip_task_id = frame_state.render_tasks.add(clip_task);
-                frame_state.render_tasks.add_dependency(
-                    frame_state.surfaces[surface_index.0].render_tasks.unwrap().port,
-                    clip_task_id,
-                );
+                let port = frame_state
+                    .surfaces[surface_index.0]
+                    .render_tasks
+                    .expect(&format!("bug: no task for surface {:?}", surface_index))
+                    .port;
+                frame_state.render_tasks.add_dependency(port, clip_task_id);
                 ClipMaskKind::Mask(clip_task_id)
             }
             None => {
                 ClipMaskKind::Clipped
             }
         }
     }
 }
@@ -1312,50 +1334,80 @@ pub enum PrimitiveInstanceKind {
         data_handle: RadialGradientDataHandle,
         visible_tiles_range: GradientTileRange,
     },
     /// Clear out a rect, used for special effects.
     Clear {
         /// Handle to the common interned data for this primitive.
         data_handle: PrimitiveDataHandle,
     },
+    /// These are non-visual instances. They are used during the
+    /// visibility pass to allow pushing/popping a clip chain
+    /// without the presence of a stacking context / picture.
+    /// TODO(gw): In some ways this seems like a hack, in some
+    ///           ways it seems reasonable. We should discuss
+    ///           other potential methods for non-visual items
+    ///           without the need for a grouping picture.
+    PushClipChain,
+    PopClipChain,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct PrimitiveVisibilityIndex(pub u32);
 
 impl PrimitiveVisibilityIndex {
     pub const INVALID: PrimitiveVisibilityIndex = PrimitiveVisibilityIndex(u32::MAX);
 }
 
 /// A bit mask describing which dirty regions a primitive is visible in.
 /// A value of 0 means not visible in any region, while a mask of 0xffff
 /// would be considered visible in all regions.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PrimitiveVisibilityMask {
     bits: u16,
 }
 
 impl PrimitiveVisibilityMask {
     /// Construct a default mask, where no regions are considered visible
     pub fn empty() -> Self {
         PrimitiveVisibilityMask {
             bits: 0,
         }
     }
 
+    pub fn all() -> Self {
+        PrimitiveVisibilityMask {
+            bits: !0,
+        }
+    }
+
+    pub fn include(&mut self, other: PrimitiveVisibilityMask) {
+        self.bits |= other.bits;
+    }
+
+    pub fn intersects(&self, other: PrimitiveVisibilityMask) -> bool {
+        (self.bits & other.bits) != 0
+    }
+
     /// Mark a given region index as visible
     pub fn set_visible(&mut self, region_index: usize) {
+        debug_assert!(region_index < PrimitiveVisibilityMask::MAX_DIRTY_REGIONS);
         self.bits |= 1 << region_index;
     }
 
     /// Returns true if there are no visible regions
     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>();
 }
 
 /// Information stored for a visible primitive about the visible
 /// rect and associated clip information.
 pub struct PrimitiveVisibility {
     /// The clip chain instance that was built for this primitive.
     pub clip_chain: ClipChainInstance,
 
@@ -1498,16 +1550,20 @@ impl PrimitiveInstance {
                 data_handle.uid()
             }
             PrimitiveInstanceKind::TextRun { data_handle, .. } => {
                 data_handle.uid()
             }
             PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
                 data_handle.uid()
             }
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain => {
+                unreachable!();
+            }
         }
     }
 }
 
 #[derive(Debug)]
 pub struct SegmentedInstance {
     pub gpu_cache_handle: GpuCacheHandle,
     pub segments_range: SegmentsRange,
@@ -1711,22 +1767,20 @@ impl PrimitiveStore {
         self.pictures[root.0].print(&self.pictures, root, &mut pt);
     }
 
     /// Destroy an existing primitive store. This is called just before
     /// a primitive store is replaced with a newly built scene.
     pub fn destroy(
         mut self,
         retained_tiles: &mut RetainedTiles,
-        clip_scroll_tree: &ClipScrollTree,
     ) {
         for pic in &mut self.pictures {
             pic.destroy(
                 retained_tiles,
-                clip_scroll_tree,
             );
         }
     }
 
     /// Returns the total count of primitive instances contained in pictures.
     pub fn prim_count(&self) -> usize {
         self.pictures
             .iter()
@@ -1735,59 +1789,73 @@ impl PrimitiveStore {
     }
 
     /// Update visibility pass - update each primitive visibility struct, and
     /// build the clip chain instance if appropriate.
     pub fn update_visibility(
         &mut self,
         pic_index: PictureIndex,
         parent_surface_index: SurfaceIndex,
+        world_culling_rect: &WorldRect,
         frame_context: &FrameVisibilityContext,
         frame_state: &mut FrameVisibilityState,
     ) -> Option<PictureRect> {
-        let (mut prim_list, surface_index, apply_local_clip_rect, raster_space) = {
+        let (mut prim_list, surface_index, apply_local_clip_rect, world_culling_rect, is_composite) = {
             let pic = &mut self.pictures[pic_index.0];
+            let mut world_culling_rect = *world_culling_rect;
 
             let prim_list = mem::replace(&mut pic.prim_list, PrimitiveList::empty());
-            let surface_index = match pic.raster_config {
-                Some(ref raster_config) => raster_config.surface_index,
-                None => parent_surface_index,
+            let (surface_index, is_composite) = match pic.raster_config {
+                Some(ref raster_config) => (raster_config.surface_index, true),
+                None => (parent_surface_index, false)
             };
 
-            if let Some(mut tile_cache) = pic.tile_cache.take() {
-                debug_assert!(frame_state.tile_cache.is_none());
-
-                // If we have a tile cache for this picture, see if any of the
-                // relative transforms have changed, which means we need to
-                // re-map the dependencies of any child primitives.
-                tile_cache.pre_update(
-                    pic.unsnapped_local_rect,
-                    frame_context,
-                    frame_state,
-                    surface_index,
-                );
-
-                frame_state.tile_cache = Some(tile_cache);
-            }
-
-            let raster_space = pic.get_raster_space(frame_context.clip_scroll_tree);
-
-            (prim_list, surface_index, pic.apply_local_clip_rect, raster_space)
+            let viewport = match pic.raster_config {
+                Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) => {
+                    let mut tile_cache = pic.tile_cache.take().unwrap();
+                    debug_assert!(frame_state.tile_cache.is_none());
+
+                    // If we have a tile cache for this picture, see if any of the
+                    // relative transforms have changed, which means we need to
+                    // re-map the dependencies of any child primitives.
+                    world_culling_rect = tile_cache.pre_update(
+                        PictureRect::from_untyped(&pic.unsnapped_local_rect.to_untyped()),
+                        surface_index,
+                        frame_context,
+                        frame_state,
+                    );
+
+                    let viewport = tile_cache.world_viewport_rect;
+
+                    frame_state.tile_cache = Some(tile_cache);
+
+                    viewport
+                }
+                _ => {
+                    WorldRect::max_rect()
+                }
+            };
+
+            if is_composite {
+                frame_state.clip_chain_stack.push_surface(viewport);
+            };
+
+            (prim_list, surface_index, pic.apply_local_clip_rect, world_culling_rect, is_composite)
         };
 
         let surface = &frame_context.surfaces[surface_index.0 as usize];
 
         let mut map_local_to_surface = surface
             .map_local_to_surface
             .clone();
 
         let map_surface_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface.surface_spatial_node_index,
-            frame_context.screen_world_rect,
+            frame_context.global_screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let mut map_local_to_raster = SpaceMapper::new(
             surface.raster_spatial_node_index,
             RasterRect::max_rect(),
         );
 
@@ -1815,68 +1883,66 @@ impl PrimitiveStore {
             );
 
             map_local_to_raster.set_target_spatial_node(
                 prim_instance.spatial_node_index,
                 frame_context.clip_scroll_tree,
             );
 
             let (is_passthrough, snap_to_visible, prim_local_rect, prim_shadow_rect) = match prim_instance.kind {
+                PrimitiveInstanceKind::PushClipChain => {
+                    frame_state.clip_chain_stack.push_clip(
+                        prim_instance.clip_chain_id,
+                        frame_state.clip_store,
+                        frame_state.data_stores,
+                        frame_context.clip_scroll_tree,
+                        frame_context.global_screen_world_rect,
+                    );
+                    continue;
+                }
+                PrimitiveInstanceKind::PopClipChain => {
+                    frame_state.clip_chain_stack.pop_clip();
+                    continue;
+                }
                 PrimitiveInstanceKind::Picture { pic_index, .. } => {
-                    let is_composite = {
-                        let pic = &self.pictures[pic_index.0];
-                        if !pic.is_visible() {
-                            continue;
-                        }
-
-                        // If this picture has a surface, we will handle any active clips from parents
-                        // when compositing this surface. Otherwise, push the clip chain from this
-                        // picture on to the active stack for any child primitive(s) to include.
-                        match pic.raster_config {
-                            Some(ref rc) => match rc.composite_mode {
-                                PictureCompositeMode::TileCache { ..} => false,
-                                _ => true,
-                            }
-                            None => false,
-                        }
-                    };
-
-                    if is_composite {
-                        frame_state.clip_chain_stack.push_surface();
-                    } else {
-                        frame_state.clip_chain_stack.push_clip(prim_instance.clip_chain_id);
+                    if !self.pictures[pic_index.0].is_visible() {
+                        continue;
                     }
 
+                    frame_state.clip_chain_stack.push_clip(
+                        prim_instance.clip_chain_id,
+                        frame_state.clip_store,
+                        frame_state.data_stores,
+                        frame_context.clip_scroll_tree,
+                        frame_context.global_screen_world_rect,
+                    );
+
                     let pic_surface_rect = self.update_visibility(
                         pic_index,
                         surface_index,
+                        &world_culling_rect,
                         frame_context,
                         frame_state,
                     );
 
+                    frame_state.clip_chain_stack.pop_clip();
+
                     let pic = &self.pictures[pic_index.0];
 
                     // The local rect of pictures is calculated dynamically based on
                     // the content of children, which may move due to the spatial
                     // node they are attached to. Other parts of the code (such as
                     // segment generation) reads the origin from the prim instance,
                     // so ensure that is kept up to date here.
                     // TODO(gw): It's unfortunate that the prim origin is duplicated
                     //           this way. In future, we could perhaps just store the
                     //           size in the picture primitive, to that there isn't
                     //           any duplicated data.
                     prim_instance.prim_origin = pic.snapped_local_rect.origin;
 
-                    // Similar to above, pop either the clip chain or root entry off the current clip stack.
-                    if is_composite {
-                        frame_state.clip_chain_stack.pop_surface();
-                    } else {
-                        frame_state.clip_chain_stack.pop_clip();
-                    }
-
                     let shadow_rect = match pic.raster_config {
                         Some(ref rc) => match rc.composite_mode {
                             // If we have a drop shadow filter, we also need to include the shadow in
                             // our local rect for the purpose of calculating the size of the picture.
                             PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
                                 let mut rect = LayoutRect::zero();
                                 for shadow in shadows {
                                     rect = rect.union(&pic.snapped_local_rect.translate(&shadow.offset));
@@ -1953,62 +2019,69 @@ impl PrimitiveStore {
                             println!("\tculled for being out of the local clip rectangle: {:?}",
                                 prim_instance.local_clip_rect);
                         }
                         continue;
                     }
                 };
 
                 // Include the clip chain for this primitive in the current stack.
-                frame_state.clip_chain_stack.push_clip(prim_instance.clip_chain_id);
-
-                if let Some(ref mut tile_cache) = frame_state.tile_cache {
-                    if !tile_cache.update_prim_dependencies(
-                        prim_instance,
-                        &frame_state.clip_chain_stack,
-                        prim_local_rect,
-                        frame_context.clip_scroll_tree,
-                        frame_state.data_stores,
-                        &frame_state.clip_store.clip_chain_nodes,
-                        &self.pictures,
-                        frame_state.resource_cache,
-                        &self.opacity_bindings,
-                        &self.images,
-                    ) {
-                        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;
-                    }
-                }
+                frame_state.clip_chain_stack.push_clip(
+                    prim_instance.clip_chain_id,
+                    frame_state.clip_store,
+                    frame_state.data_stores,
+                    frame_context.clip_scroll_tree,
+                    frame_context.global_screen_world_rect,
+                );
 
                 frame_state.clip_store.set_active_clips(
                     prim_instance.local_clip_rect,
                     prim_instance.spatial_node_index,
-                    frame_state.clip_chain_stack.current_clips(),
+                    frame_state.clip_chain_stack.current_clips_array(),
                     &frame_context.clip_scroll_tree,
                     &mut frame_state.data_stores.clip,
                 );
 
                 let clip_chain = frame_state
                     .clip_store
                     .build_clip_chain_instance(
                         local_rect,
                         &map_local_to_surface,
                         &map_surface_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         surface.device_pixel_scale,
-                        &frame_context.screen_world_rect,
+                        &world_culling_rect,
                         &mut frame_state.data_stores.clip,
                         true,
                     );
 
+                if let Some(ref mut tile_cache) = frame_state.tile_cache {
+                    if !tile_cache.update_prim_dependencies(
+                        prim_instance,
+                        clip_chain.as_ref(),
+                        prim_local_rect,
+                        frame_context.clip_scroll_tree,
+                        frame_state.data_stores,
+                        frame_state.clip_store,
+                        &self.pictures,
+                        frame_state.resource_cache,
+                        &self.opacity_bindings,
+                        &self.images,
+                        surface_index,
+                    ) {
+                        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,
                     None => {
                         if prim_instance.is_chased() {
                             println!("\tunable to build the clip chain, skipping");
@@ -2029,17 +2102,17 @@ impl PrimitiveStore {
                 // This includes both the prim bounding rect + local prim clip rect!
                 let world_rect = match map_surface_to_world.map(&clip_chain.pic_clip_rect) {
                     Some(world_rect) => world_rect,
                     None => {
                         continue;
                     }
                 };
 
-                let clipped_world_rect = match world_rect.intersection(&frame_context.screen_world_rect) {
+                let clipped_world_rect = match world_rect.intersection(&world_culling_rect) {
                     Some(rect) => rect,
                     None => {
                         continue;
                     }
                 };
 
                 let combined_local_clip_rect = if apply_local_clip_rect {
                     clip_chain.local_clip_rect
@@ -2106,16 +2179,18 @@ impl PrimitiveStore {
                 if let Some(rect) = map_local_to_surface.map(&combined_visible_rect) {
                     surface_rect = surface_rect.union(&rect);
                 }
 
                 // When the debug display is enabled, paint a colored rectangle around each
                 // primitive.
                 if frame_context.debug_flags.contains(::api::DebugFlags::PRIMITIVE_DBG) {
                     let debug_color = match prim_instance.kind {
+                        PrimitiveInstanceKind::PushClipChain |
+                        PrimitiveInstanceKind::PopClipChain |
                         PrimitiveInstanceKind::Picture { .. } => ColorF::TRANSPARENT,
                         PrimitiveInstanceKind::TextRun { .. } => debug_colors::RED,
                         PrimitiveInstanceKind::LineDecoration { .. } => debug_colors::PURPLE,
                         PrimitiveInstanceKind::NormalBorder { .. } |
                         PrimitiveInstanceKind::ImageBorder { .. } => debug_colors::ORANGE,
                         PrimitiveInstanceKind::Rectangle { .. } => ColorF { r: 0.8, g: 0.8, b: 0.8, a: 0.5 },
                         PrimitiveInstanceKind::YuvImage { .. } => debug_colors::BLUE,
                         PrimitiveInstanceKind::Image { .. } => debug_colors::BLUE,
@@ -2142,25 +2217,27 @@ impl PrimitiveStore {
                         visibility_mask: PrimitiveVisibilityMask::empty(),
                     }
                 );
 
                 prim_instance.visibility_info = vis_index;
 
                 self.request_resources_for_prim(
                     prim_instance,
-                    surface,
-                    raster_space,
                     &map_local_to_surface,
-                    frame_context,
                     frame_state,
                 );
             }
         }
 
+        // Similar to above, pop either the clip chain or root entry off the current clip stack.
+        if is_composite {
+            frame_state.clip_chain_stack.pop_surface();
+        }
+
         let pic = &mut self.pictures[pic_index.0];
         pic.prim_list = prim_list;
 
         // If the local rect changed (due to transforms in child primitives) then
         // invalidate the GPU cache location to re-upload the new local rect
         // and stretch size. Drop shadow filters also depend on the local rect
         // size for the extra GPU cache data handle.
         // TODO(gw): In future, if we support specifying a flag which gets the
@@ -2225,46 +2302,26 @@ impl PrimitiveStore {
             );
             map_surface_to_parent_surface.map(&surface_rect)
         }
     }
 
     fn request_resources_for_prim(
         &mut self,
         prim_instance: &mut PrimitiveInstance,
-        surface: &SurfaceInfo,
-        raster_space: RasterSpace,
         map_local_to_surface: &SpaceMapper<LayoutPixel, PicturePixel>,
-        frame_context: &FrameVisibilityContext,
         frame_state: &mut FrameVisibilityState,
     ) {
         match prim_instance.kind {
-            PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
-                let prim_data = &mut frame_state.data_stores.text_run[data_handle];
-                let run = &mut self.text_runs[run_index];
-
-                // The transform only makes sense for screen space rasterization
-                let relative_transform = frame_context
-                    .clip_scroll_tree
-                    .get_world_transform(prim_instance.spatial_node_index)
-                    .into_transform();
-                let prim_offset = prim_instance.prim_origin.to_vector() - run.reference_frame_relative_offset;
-
-                run.request_resources(
-                    prim_offset,
-                    &prim_data.font,
-                    &prim_data.glyphs,
-                    &relative_transform,
-                    surface,
-                    raster_space,
-                    frame_state.resource_cache,
-                    frame_state.gpu_cache,
-                    frame_state.render_tasks,
-                    frame_state.scratch,
-                );
+            PrimitiveInstanceKind::TextRun { .. } => {
+                // Text runs can't request resources early here, as we don't
+                // know until TileCache::post_update() whether we are drawing
+                // on an opaque surface.
+                // TODO(gw): We might be able to detect simple cases of this earlier,
+                //           during the picture traversal. But it's probably not worth it?
             }
             PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
                 let prim_data = &mut frame_state.data_stores.image[data_handle];
                 let common_data = &mut prim_data.common;
                 let image_data = &mut prim_data.kind;
                 let image_instance = &mut self.images[image_instance_index];
 
                 let image_properties = frame_state
@@ -2412,16 +2469,18 @@ impl PrimitiveStore {
             }
             PrimitiveInstanceKind::Clear { .. } |
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } |
             PrimitiveInstanceKind::YuvImage { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 // These prims don't support opacity collapse
             }
             PrimitiveInstanceKind::Picture { pic_index, .. } => {
                 let pic = &self.pictures[pic_index.0];
 
                 // If we encounter a picture that is a pass-through
                 // (i.e. no composite mode), then we can recurse into
@@ -2523,16 +2582,17 @@ impl PrimitiveStore {
                         .clipped_world_rect;
 
                     match pic.take_context(
                         pic_index,
                         clipped_prim_bounding_rect,
                         pic_context.surface_spatial_node_index,
                         pic_context.raster_spatial_node_index,
                         pic_context.surface_index,
+                        pic_context.subpixel_mode,
                         frame_state,
                         frame_context,
                     ) {
                         Some(info) => Some(info),
                         None => {
                             if prim_instance.is_chased() {
                                 println!("\tculled for carrying an invisible composite filter");
                             }
@@ -2547,16 +2607,18 @@ impl PrimitiveStore {
                 PrimitiveInstanceKind::Rectangle { .. } |
                 PrimitiveInstanceKind::LineDecoration { .. } |
                 PrimitiveInstanceKind::NormalBorder { .. } |
                 PrimitiveInstanceKind::ImageBorder { .. } |
                 PrimitiveInstanceKind::YuvImage { .. } |
                 PrimitiveInstanceKind::Image { .. } |
                 PrimitiveInstanceKind::LinearGradient { .. } |
                 PrimitiveInstanceKind::RadialGradient { .. } |
+                PrimitiveInstanceKind::PushClipChain |
+                PrimitiveInstanceKind::PopClipChain |
                 PrimitiveInstanceKind::Clear { .. } => {
                     None
                 }
             }
         };
 
         let is_passthrough = match pic_info {
             Some((pic_context_for_children, mut pic_state_for_children, mut prim_list)) => {
@@ -2643,46 +2705,25 @@ impl PrimitiveStore {
             // However, it's possible that the dirty rect has got smaller, if tiles were not
             // dirty. Intersecting with the dirty rect here eliminates preparing any primitives
             // outside the dirty rect, and reduces the size of any off-screen surface allocations
             // for clip masks / render tasks that we make.
             {
                 let visibility_info = &mut scratch.prim_info[prim_instance.visibility_info.0 as usize];
                 let dirty_region = frame_state.current_dirty_region();
 
-                // Check if the primitive world rect intersects with the overall dirty rect first.
-                match visibility_info.clipped_world_rect.intersection(&dirty_region.combined.world_rect) {
-                    Some(rect) => {
-                        // It does intersect the overall dirty rect, so it *might* be visible.
-                        // Store this reduced rect here, which is used for clip mask and other
-                        // render task size calculations. In future, we may consider creating multiple
-                        // render task graphs, one per dirty region.
-                        visibility_info.clipped_world_rect = rect;
-
-                        // If there is more than one dirty region, it's possible that this primitive
-                        // is inside the overal dirty rect, but doesn't intersect any of the individual
-                        // dirty rects. If that's the case, then we can skip drawing this primitive too.
-                        if dirty_region.dirty_rects.len() > 1 {
-                            for (region_index, region) in dirty_region.dirty_rects.iter().enumerate() {
-                                if visibility_info.clipped_world_rect.intersects(&region.world_rect) {
-                                    visibility_info.visibility_mask.set_visible(region_index);
-                                }
-                            }
-
-                            if visibility_info.visibility_mask.is_empty() {
-                                prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
-                                continue;
-                            }
-                        }
+                for dirty_region in &dirty_region.dirty_rects {
+                    if visibility_info.clipped_world_rect.intersects(&dirty_region.world_rect) {
+                        visibility_info.visibility_mask.include(dirty_region.visibility_mask);
                     }
-                    None => {
-                        // Outside the overall dirty rect, so can be skipped.
-                        prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
-                        continue;
-                    }
+                }
+
+                if visibility_info.visibility_mask.is_empty() {
+                    prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
+                    continue;
                 }
             }
 
             pic_state.map_local_to_pic.set_target_spatial_node(
                 prim_instance.spatial_node_index,
                 frame_context.clip_scroll_tree,
             );
 
@@ -2763,18 +2804,44 @@ impl PrimitiveStore {
                                 cache_key.wavy_line_thickness.to_f32_px(),
                                 LayoutSize::from_au(cache_key.size),
                             );
                             render_tasks.add(task)
                         }
                     ));
                 }
             }
-            PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+            PrimitiveInstanceKind::TextRun { run_index, data_handle, .. } => {
                 let prim_data = &mut data_stores.text_run[*data_handle];
+                let run = &mut self.text_runs[*run_index];
+
+                // The transform only makes sense for screen space rasterization
+                let relative_transform = frame_context
+                    .clip_scroll_tree
+                    .get_world_transform(prim_instance.spatial_node_index)
+                    .into_transform();
+                let prim_offset = prim_instance.prim_origin.to_vector() - run.reference_frame_relative_offset;
+
+                let pic = &self.pictures[pic_context.pic_index.0];
+                let raster_space = pic.get_raster_space(frame_context.clip_scroll_tree);
+                let surface = &frame_state.surfaces[pic_context.surface_index.0];
+
+                run.request_resources(
+                    prim_offset,
+                    &prim_data.font,
+                    &prim_data.glyphs,
+                    &relative_transform,
+                    surface,
+                    raster_space,
+                    pic_context.subpixel_mode,
+                    frame_state.resource_cache,
+                    frame_state.gpu_cache,
+                    frame_state.render_tasks,
+                    scratch,
+                );
 
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
                 prim_data.update(frame_state);
             }
             PrimitiveInstanceKind::Clear { data_handle, .. } => {
                 let prim_data = &mut data_stores.prim[*data_handle];
 
@@ -3106,17 +3173,17 @@ impl PrimitiveStore {
                 ) {
                     if let Some(ref mut splitter) = pic_state.plane_splitter {
                         PicturePrimitive::add_split_plane(
                             splitter,
                             frame_context.clip_scroll_tree,
                             prim_instance.spatial_node_index,
                             pic.snapped_local_rect,
                             &prim_info.combined_local_clip_rect,
-                            frame_context.screen_world_rect,
+                            frame_context.global_screen_world_rect,
                             plane_split_anchor,
                         );
                     }
 
                     // If this picture uses segments, ensure the GPU cache is
                     // up to date with segment local rects.
                     // TODO(gw): This entire match statement above can now be
                     //           refactored into prepare_interned_prim_for_render.
@@ -3137,16 +3204,18 @@ impl PrimitiveStore {
                                 ]);
                             }
                         );
                     }
                 } else {
                     prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                 }
             }
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain => {}
         };
     }
 }
 
 fn write_segment<F>(
     segment_instance_index: SegmentInstanceIndex,
     frame_state: &mut FrameBuildingState,
     segments: &mut SegmentStorage,
@@ -3342,18 +3411,23 @@ impl<'a> GpuDataRequest<'a> {
                             -0.5 * info.original_alloc_size.height,
                         ),
                         inner_clip_mode,
                     );
 
                     continue;
                 }
                 ClipItem::Image { .. } => {
-                    rect_clips_only = false;
-                    continue;
+                    // If we encounter an image mask, bail out from segment building.
+                    // It's not possible to know which parts of the primitive are affected
+                    // by the mask (without inspecting the pixels). We could do something
+                    // better here in the future if it ever shows up as a performance issue
+                    // (for instance, at least segment based on the bounding rect of the
+                    // image mask if it's non-repeating).
+                    return false;
                 }
             };
 
             segment_builder.push_clip_rect(local_clip_rect, radius, mode);
         }
 
         if is_large || rect_clips_only {
             // If there were no local clips, then we will subdivide the primitive into
@@ -3455,16 +3529,18 @@ impl PrimitiveInstance {
                 }
             }
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } |
             PrimitiveInstanceKind::Clear { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 // These primitives don't support / need segments.
                 return;
             }
         };
 
         if *segment_instance_index == SegmentInstanceIndex::INVALID {
             let mut segments: SmallVec<[BrushSegment; 8]> = SmallVec::new();
@@ -3519,16 +3595,18 @@ impl PrimitiveInstance {
         segment_instances_store: &mut SegmentInstanceStorage,
         clip_mask_instances: &mut Vec<ClipMaskKind>,
         unclipped: &DeviceRect,
         device_pixel_scale: DevicePixelScale,
     ) -> bool {
         let segments = match self.kind {
             PrimitiveInstanceKind::TextRun { .. } |
             PrimitiveInstanceKind::Clear { .. } |
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain |
             PrimitiveInstanceKind::LineDecoration { .. } => {
                 return false;
             }
             PrimitiveInstanceKind::Image { image_instance_index, .. } => {
                 let segment_instance_index = prim_store
                     .images[image_instance_index]
                     .segment_instance_index;
 
@@ -3628,17 +3706,17 @@ impl PrimitiveInstance {
                 frame_state,
                 &mut data_stores.clip,
                 unclipped,
                 prim_info.snap_offsets,
                 device_pixel_scale,
             );
             clip_mask_instances.push(clip_mask_kind);
         } else {
-            let dirty_world_rect = frame_state.current_dirty_region().combined.world_rect;
+            let dirty_world_rect = frame_state.current_dirty_region().combined;
 
             for segment in segments {
                 // Build a clip chain for the smaller segment rect. This will
                 // often manage to eliminate most/all clips, and sometimes
                 // clip the segment completely.
                 frame_state.clip_store.set_active_clips_from_clip_chain(
                     &prim_info.clip_chain,
                     self.spatial_node_index,
--- a/gfx/wr/webrender/src/prim_store/text_run.rs
+++ b/gfx/wr/webrender/src/prim_store/text_run.rs
@@ -5,17 +5,17 @@
 use api::{ColorF, GlyphInstance, RasterSpace, Shadow};
 use api::units::{DevicePixelScale, LayoutToWorldTransform, LayoutVector2D};
 use crate::display_list_flattener::{CreateShadow, IsVisible};
 use crate::frame_builder::FrameBuildingState;
 use crate::glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use crate::gpu_cache::GpuCache;
 use crate::intern;
 use crate::internal_types::LayoutPrimitiveInfo;
-use crate::picture::SurfaceInfo;
+use crate::picture::{SubpixelMode, SurfaceInfo};
 use crate::prim_store::{PrimitiveOpacity, PrimitiveSceneData,  PrimitiveScratchBuffer};
 use crate::prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData};
 use crate::render_task::{RenderTaskGraph};
 use crate::renderer::{MAX_VERTEX_TEXTURE_WIDTH};
 use crate::resource_cache::{ResourceCache};
 use crate::util::{MatrixHelpers};
 use crate::prim_store::{InternablePrimitive, PrimitiveInstanceKind};
 use std::ops;
@@ -216,17 +216,17 @@ pub struct TextRunPrimitive {
 }
 
 impl TextRunPrimitive {
     pub fn update_font_instance(
         &mut self,
         specified_font: &FontInstance,
         device_pixel_scale: DevicePixelScale,
         transform: &LayoutToWorldTransform,
-        allow_subpixel_aa: bool,
+        subpixel_mode: SubpixelMode,
         raster_space: RasterSpace,
     ) -> bool {
         // If local raster space is specified, include that in the scale
         // of the glyphs that get rasterized.
         // TODO(gw): Once we support proper local space raster modes, this
         //           will implicitly be part of the device pixel ratio for
         //           the (cached) local space surface, and so this code
         //           will no longer be required.
@@ -269,45 +269,46 @@ impl TextRunPrimitive {
             transform: font_transform,
             size: device_font_size,
             ..specified_font.clone()
         };
 
         // If subpixel AA is disabled due to the backing surface the glyphs
         // are being drawn onto, disable it (unless we are using the
         // specifial subpixel mode that estimates background color).
-        if (!allow_subpixel_aa && self.used_font.bg_color.a == 0) ||
+        if (subpixel_mode == SubpixelMode::Deny && self.used_font.bg_color.a == 0) ||
             // If using local space glyphs, we don't want subpixel AA.
             !transform_glyphs {
             self.used_font.disable_subpixel_aa();
         }
 
         cache_dirty
     }
 
     pub fn request_resources(
         &mut self,
         prim_offset: LayoutVector2D,
         specified_font: &FontInstance,
         glyphs: &[GlyphInstance],
         transform: &LayoutToWorldTransform,
         surface: &SurfaceInfo,
         raster_space: RasterSpace,
+        subpixel_mode: SubpixelMode,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskGraph,
         scratch: &mut PrimitiveScratchBuffer,
     ) {
         let device_pixel_scale = surface.device_pixel_scale;
 
         let cache_dirty = self.update_font_instance(
             specified_font,
             device_pixel_scale,
             transform,
-            surface.allow_subpixel_aa,
+            subpixel_mode,
             raster_space,
         );
 
         if self.glyph_keys_range.is_empty() || cache_dirty {
             let subpx_dir = self.used_font.get_subpx_dir();
 
             self.glyph_keys_range = scratch.glyph_keys.extend(
                 glyphs.iter().map(|src| {
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -292,16 +292,20 @@ impl DataStores {
             PrimitiveInstanceKind::TextRun { data_handle, .. }  => {
                 let prim_data = &self.text_run[data_handle];
                 &prim_data.common
             }
             PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
                 let prim_data = &self.yuv_image[data_handle];
                 &prim_data.common
             }
+            PrimitiveInstanceKind::PushClipChain |
+            PrimitiveInstanceKind::PopClipChain => {
+                unreachable!();
+            }
         }
     }
 }
 
 struct Document {
     // The id of this document
     id: DocumentId,
     // The latest built scene, usable to build frames.
@@ -622,17 +626,16 @@ impl Document {
 
         // Give the old frame builder a chance to destroy any resources.
         // Right now, all this does is build a hash map of any cached
         // surface tiles, that can be provided to the next frame builder.
         let mut retained_tiles = RetainedTiles::new();
         if let Some(frame_builder) = self.frame_builder.take() {
             let globals = frame_builder.destroy(
                 &mut retained_tiles,
-                &self.clip_scroll_tree,
             );
 
             // Provide any cached tiles from the previous frame builder to
             // the newly built one.
             built_scene.frame_builder.set_retained_resources(
                 retained_tiles,
                 globals,
             );
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -14,20 +14,20 @@ use crate::clip_scroll_tree::SpatialNode
 use crate::device::TextureFilter;
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use crate::frame_builder::FrameBuilderConfig;
 use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use crate::glyph_rasterizer::GpuGlyphCacheKey;
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::{BorderInstance, ImageSource, UvRectKind, SnapOffsets};
-use crate::internal_types::{CacheTextureId, FastHashMap, LayerIndex, SavedTargetIndex};
+use crate::internal_types::{CacheTextureId, FastHashMap, LayerIndex, SavedTargetIndex, TextureSource};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
-use crate::prim_store::PictureIndex;
+use crate::prim_store::{PictureIndex, PrimitiveVisibilityMask};
 use crate::prim_store::image::ImageCacheKey;
 use crate::prim_store::gradient::{GRADIENT_FP_STOPS, GradientCacheKey, GradientStopKey};
 use crate::prim_store::line_dec::LineDecorationCacheKey;
 #[cfg(feature = "debugger")]
 use crate::print_tree::{PrintTreePrinter};
 use crate::render_backend::FrameId;
 use crate::resource_cache::{CacheItem, ResourceCache};
 use std::{ops, mem, usize, f32, i32, u32};
@@ -70,17 +70,16 @@ impl RenderTaskId {
 }
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[repr(C)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTaskAddress(pub u16);
 
-#[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTaskGraph {
     pub tasks: Vec<RenderTask>,
     pub task_data: Vec<RenderTaskData>,
     /// Tasks that don't have dependencies, and that may be shared between
     /// picture tasks.
     ///
@@ -452,32 +451,43 @@ pub enum RenderTaskLocation {
     TextureCache {
         /// Which texture in the texture cache should be drawn into.
         texture: CacheTextureId,
         /// The target layer in the above texture.
         layer: LayerIndex,
         /// The target region within the above layer.
         rect: DeviceIntRect,
     },
+    /// This render task will be drawn to a picture cache texture that is
+    /// persisted between both frames and scenes, if the content remains valid.
+    PictureCache {
+        /// The texture ID to draw to.
+        texture: TextureSource,
+        /// Slice index in the texture array to draw to.
+        layer: i32,
+        /// Size in device pixels of this picture cache tile.
+        size: DeviceIntSize,
+    },
 }
 
 impl RenderTaskLocation {
     /// Returns true if this is a dynamic location.
     pub fn is_dynamic(&self) -> bool {
         match *self {
             RenderTaskLocation::Dynamic(..) => true,
             _ => false,
         }
     }
 
     pub fn size(&self) -> DeviceIntSize {
         match self {
             RenderTaskLocation::Fixed(rect) => rect.size,
             RenderTaskLocation::Dynamic(_, size) => *size,
             RenderTaskLocation::TextureCache { rect, .. } => rect.size,
+            RenderTaskLocation::PictureCache { size, .. } => *size,
         }
     }
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CacheMaskTask {
@@ -492,38 +502,29 @@ pub struct CacheMaskTask {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipRegionTask {
     pub clip_data_address: GpuCacheAddress,
     pub local_pos: LayoutPoint,
     pub device_pixel_scale: DevicePixelScale,
 }
 
-#[derive(Debug)]
-#[cfg_attr(feature = "capture", derive(Serialize))]
-#[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct TileBlit {
-    pub target: CacheItem,
-    pub src_offset: DeviceIntPoint,
-    pub dest_offset: DeviceIntPoint,
-    pub size: DeviceIntSize,
-}
-
-#[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PictureTask {
     pub pic_index: PictureIndex,
     pub can_merge: bool,
     pub content_origin: DeviceIntPoint,
     pub uv_rect_handle: GpuCacheHandle,
-    pub root_spatial_node_index: SpatialNodeIndex,
     pub surface_spatial_node_index: SpatialNodeIndex,
     uv_rect_kind: UvRectKind,
     device_pixel_scale: DevicePixelScale,
+    /// A bitfield that describes which dirty regions should be included
+    /// in batches built for this picture task.
+    pub vis_mask: PrimitiveVisibilityMask,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BlurTask {
     pub blur_std_deviation: f32,
     pub target_kind: RenderTargetKind,
@@ -616,17 +617,16 @@ pub struct LineDecorationTask {
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTaskData {
     pub data: [f32; FLOATS_PER_RENDER_TASK_INFO],
 }
 
-#[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum RenderTaskKind {
     Picture(PictureTask),
     CacheMask(CacheMaskTask),
     ClipRegion(ClipRegionTask),
     VerticalBlur(BlurTask),
     HorizontalBlur(BlurTask),
@@ -698,17 +698,16 @@ impl BlurTaskKey {
         // a lot of precision.
         const QUANTIZATION_FACTOR: f32 = 1024.0;
         let stddev_x = (blur_stddev.width * QUANTIZATION_FACTOR) as u32;
         let stddev_y = (blur_stddev.height * QUANTIZATION_FACTOR) as u32;
         BlurTaskKey::Blur { downscale_level, stddev_x, stddev_y }
     }
 }
 
-#[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTask {
     pub location: RenderTaskLocation,
     pub children: Vec<RenderTaskId>,
     pub kind: RenderTaskKind,
     pub clear_mode: ClearMode,
     pub saved_index: Option<SavedTargetIndex>,
@@ -749,43 +748,44 @@ impl RenderTask {
     }
 
     pub fn new_picture(
         location: RenderTaskLocation,
         unclipped_size: DeviceSize,
         pic_index: PictureIndex,
         content_origin: DeviceIntPoint,
         uv_rect_kind: UvRectKind,
-        root_spatial_node_index: SpatialNodeIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         device_pixel_scale: DevicePixelScale,
+        vis_mask: PrimitiveVisibilityMask,
     ) -> Self {
         let size = match location {
             RenderTaskLocation::Dynamic(_, size) => size,
             RenderTaskLocation::Fixed(rect) => rect.size,
             RenderTaskLocation::TextureCache { rect, .. } => rect.size,
+            RenderTaskLocation::PictureCache { size, .. } => size,
         };
 
         render_task_sanity_check(&size);
 
         let can_merge = size.width as f32 >= unclipped_size.width &&
                         size.height as f32 >= unclipped_size.height;
 
         RenderTask {
             location,
             children: Vec::new(),
             kind: RenderTaskKind::Picture(PictureTask {
                 pic_index,
                 content_origin,
                 can_merge,
                 uv_rect_handle: GpuCacheHandle::new(),
                 uv_rect_kind,
-                root_spatial_node_index,
                 surface_spatial_node_index,
                 device_pixel_scale,
+                vis_mask,
             }),
             clear_mode: ClearMode::Transparent,
             saved_index: None,
         }
     }
 
     pub fn new_gradient(
         size: DeviceIntSize,
@@ -1342,16 +1342,17 @@ impl RenderTask {
         }
     }
 
     pub fn get_dynamic_size(&self) -> DeviceIntSize {
         match self.location {
             RenderTaskLocation::Fixed(..) => DeviceIntSize::zero(),
             RenderTaskLocation::Dynamic(_, size) => size,
             RenderTaskLocation::TextureCache { rect, .. } => rect.size,
+            RenderTaskLocation::PictureCache { size, .. } => size,
         }
     }
 
     pub fn get_target_rect(&self) -> (DeviceIntRect, RenderTargetIndex) {
         match self.location {
             RenderTaskLocation::Fixed(rect) => {
                 (rect, RenderTargetIndex(0))
             }
@@ -1373,16 +1374,25 @@ impl RenderTask {
                 (DeviceIntRect::new(origin, size), target_index)
             }
             RenderTaskLocation::Dynamic(None, _) => {
                 (DeviceIntRect::zero(), RenderTargetIndex(0))
             }
             RenderTaskLocation::TextureCache {layer, rect, .. } => {
                 (rect, RenderTargetIndex(layer as usize))
             }
+            RenderTaskLocation::PictureCache { size, layer, .. } => {
+                (
+                    DeviceIntRect::new(
+                        DeviceIntPoint::zero(),
+                        size,
+                    ),
+                    RenderTargetIndex(layer as usize),
+                )
+            }
         }
     }
 
     pub fn target_kind(&self) -> RenderTargetKind {
         match self.kind {
             RenderTaskKind::Readback(..) => RenderTargetKind::Color,
 
             RenderTaskKind::LineDecoration(..) => RenderTargetKind::Color,
@@ -1530,17 +1540,18 @@ impl RenderTask {
 
     /// Mark this render task for keeping the results alive up until the end of the frame.
     pub fn mark_for_saving(&mut self) {
         match self.location {
             RenderTaskLocation::Fixed(..) |
             RenderTaskLocation::Dynamic(..) => {
                 self.saved_index = Some(SavedTargetIndex::PENDING);
             }
-            RenderTaskLocation::TextureCache { .. } => {
+            RenderTaskLocation::TextureCache { .. } |
+            RenderTaskLocation::PictureCache { .. } => {
                 panic!("Unable to mark a permanently cached task for saving!");
             }
         }
     }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -1640,16 +1651,17 @@ impl RenderTaskCache {
         render_tasks: &mut RenderTaskGraph,
     ) {
         let render_task = &mut render_tasks[render_task_id];
         let target_kind = render_task.target_kind();
 
         // Find out what size to alloc in the texture cache.
         let size = match render_task.location {
             RenderTaskLocation::Fixed(..) |
+            RenderTaskLocation::PictureCache { .. } |
             RenderTaskLocation::TextureCache { .. } => {
                 panic!("BUG: dynamic task was expected");
             }
             RenderTaskLocation::Dynamic(_, size) => size,
         };
 
         // Select the right texture page to allocate from.
         let image_format = match target_kind {
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -39,43 +39,42 @@ use api::{DocumentId, Epoch, ExternalIma
 use api::{ExternalImageType, FontRenderMode, FrameMsg, ImageFormat, PipelineId};
 use api::{ImageRendering, Checkpoint, NotificationRequest};
 use api::{DebugCommand, MemoryReport, VoidPtrToSizeFn};
 use api::{RenderApiSender, RenderNotifier, TextureTarget};
 use api::channel;
 use api::units::*;
 pub use api::DebugFlags;
 use api::channel::{MsgSender, PayloadReceiverHelperMethods};
-use crate::batch::{BatchKind, BatchTextures, BrushBatchKind, ClipBatchList};
+use crate::batch::{AlphaBatchContainer, BatchKind, BatchTextures, BrushBatchKind, ClipBatchList};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use crate::capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
 use crate::debug_colors;
 use crate::debug_render::{DebugItem, DebugRenderer};
 use crate::device::{DepthFunction, Device, GpuFrameId, Program, UploadMethod, Texture, PBO};
 use crate::device::{DrawTarget, ExternalTexture, FBOId, ReadTarget, TextureSlot};
 use crate::device::{ShaderError, TextureFilter, TextureFlags,
              VertexUsageHint, VAO, VBO, CustomVAO};
 use crate::device::{ProgramCache};
 use crate::device::query::GpuTimer;
-use euclid::rect;
-use euclid::{Transform3D, TypedScale};
+use euclid::{rect, Transform3D, TypedScale};
 use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
 use gleam::gl;
 use crate::glyph_rasterizer::{GlyphFormat, GlyphRasterizer};
 use crate::gpu_cache::{GpuBlockData, GpuCacheUpdate, GpuCacheUpdateList};
 use crate::gpu_cache::{GpuCacheDebugChunk, GpuCacheDebugCmd};
 #[cfg(feature = "pathfinder")]
 use crate::gpu_glyph_renderer::GpuGlyphRenderer;
 use crate::gpu_types::{PrimitiveHeaderI, PrimitiveHeaderF, ScalingInstance, TransformData, ResolveInstanceData};
 use crate::internal_types::{TextureSource, ORTHO_FAR_PLANE, ORTHO_NEAR_PLANE, ResourceCacheError};
 use crate::internal_types::{CacheTextureId, DebugOutput, FastHashMap, FastHashSet, LayerIndex, RenderedDocument, ResultMsg};
 use crate::internal_types::{TextureCacheAllocationKind, TextureCacheUpdate, TextureUpdateList, TextureUpdateSource};
 use crate::internal_types::{RenderTargetInfo, SavedTargetIndex};
 use malloc_size_of::MallocSizeOfOps;
-use crate::picture::{RecordedDirtyRegion, TileCache};
+use crate::picture::{RecordedDirtyRegion, TILE_SIZE_WIDTH, TILE_SIZE_HEIGHT};
 use crate::prim_store::DeferredResolve;
 use crate::profiler::{BackendProfileCounters, FrameProfileCounters, TimeProfileCounter,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
 use crate::profiler::{Profiler, ChangeIndicator};
 use crate::device::query::{GpuProfiler, GpuDebugMethod};
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use crate::record::ApiRecordingReceiver;
 use crate::render_backend::{FrameId, RenderBackend};
@@ -99,17 +98,17 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{channel, Receiver};
 use std::thread;
 use std::cell::RefCell;
 use crate::texture_cache::TextureCache;
 use thread_profiler::{register_thread_with_profiler, write_profile};
-use crate::tiling::{AlphaRenderTarget, ColorRenderTarget};
+use crate::tiling::{AlphaRenderTarget, ColorRenderTarget, PictureCacheTarget};
 use crate::tiling::{BlitJob, BlitJobSource, RenderPassKind, RenderTargetList};
 use crate::tiling::{Frame, RenderTarget, RenderTargetKind, TextureCacheRenderTarget};
 #[cfg(not(feature = "pathfinder"))]
 use crate::tiling::GlyphJob;
 use time::precise_time_ns;
 
 cfg_if! {
     if #[cfg(feature = "debugger")] {
@@ -204,17 +203,16 @@ const GPU_TAG_BLUR: GpuProfileTag = GpuP
 const GPU_TAG_BLIT: GpuProfileTag = GpuProfileTag {
     label: "Blit",
     color: debug_colors::LIME,
 };
 const GPU_TAG_SCALE: GpuProfileTag = GpuProfileTag {
     label: "Scale",
     color: debug_colors::GHOSTWHITE,
 };
-
 const GPU_SAMPLER_TAG_ALPHA: GpuProfileTag = GpuProfileTag {
     label: "Alpha Targets",
     color: debug_colors::BLACK,
 };
 const GPU_SAMPLER_TAG_OPAQUE: GpuProfileTag = GpuProfileTag {
     label: "Opaque Pass",
     color: debug_colors::BLACK,
 };
@@ -1971,16 +1969,17 @@ impl Renderer {
             dual_source_blending_is_supported: use_dual_source_blending,
             chase_primitive: options.chase_primitive,
             enable_picture_caching: options.enable_picture_caching,
             testing: options.testing,
             gpu_supports_fast_clears: options.gpu_supports_fast_clears,
             gpu_supports_advanced_blend: ext_blend_equation_advanced,
             advanced_blend_is_coherent: ext_blend_equation_advanced_coherent,
             batch_lookback_count: options.batch_lookback_count,
+            background_color: options.clear_color,
         };
         info!("WR {:?}", config);
 
         let device_pixel_ratio = options.device_pixel_ratio;
         let debug_flags = options.debug_flags;
         let payload_rx_for_backend = payload_rx.to_mpsc_receiver();
         let size_of_op = options.size_of_op;
         let enclosing_size_of_op = options.enclosing_size_of_op;
@@ -2075,17 +2074,17 @@ impl Renderer {
             if let Some(ref thread_listener) = *thread_listener_for_render_backend {
                 thread_listener.thread_started(&rb_thread_name);
             }
 
             let texture_cache = TextureCache::new(
                 max_texture_size,
                 max_texture_layers,
                 if config.enable_picture_caching {
-                    Some(TileCache::tile_dimensions(config.testing))
+                    Some(DeviceIntSize::new(TILE_SIZE_WIDTH, TILE_SIZE_HEIGHT))
                 } else {
                     None
                 },
                 start_size,
             );
 
             let resource_cache = ResourceCache::new(
                 texture_cache,
@@ -2549,20 +2548,20 @@ impl Renderer {
 
         for &(_, ref render_doc) in &self.active_documents {
             for pass in &render_doc.frame.passes {
                 let mut debug_targets = Vec::new();
                 match pass.kind {
                     RenderPassKind::MainFramebuffer { ref main_target, .. } => {
                         debug_targets.push(Self::debug_color_target(main_target));
                     }
-                    RenderPassKind::OffScreen { ref alpha, ref color, ref texture_cache } => {
+                    RenderPassKind::OffScreen { ref alpha, ref color, ref texture_cache, .. } => {
                         debug_targets.extend(alpha.targets.iter().map(Self::debug_alpha_target));
                         debug_targets.extend(color.targets.iter().map(Self::debug_color_target));
-                        debug_targets.extend(texture_cache.iter().map(|(_, target)| Self::debug_texture_cache_target(target)))
+                        debug_targets.extend(texture_cache.iter().map(|(_, target)| Self::debug_texture_cache_target(target)));
                     }
                 }
 
                 debug_passes.add(debug_server::Pass { targets: debug_targets });
             }
         }
 
         serde_json::to_string(&debug_passes).unwrap()
@@ -3023,17 +3022,17 @@ impl Renderer {
                             let mut texture = self.device.create_texture(
                                 TextureTarget::Array,
                                 info.format,
                                 info.width,
                                 info.height,
                                 info.filter,
                                 // This needs to be a render target because some render
                                 // tasks get rendered into the texture cache.
-                                Some(RenderTargetInfo { has_depth: false }),
+                                Some(RenderTargetInfo { has_depth: info.has_depth }),
                                 info.layer_count,
                             );
 
                             if info.is_shared_cache {
                                 texture.flags_mut()
                                     .insert(TextureFlags::IS_SHARED_TEXTURE_CACHE);
 
                                 // Textures in the cache generally don't need to be cleared,
@@ -3372,16 +3371,266 @@ impl Renderer {
         self.draw_instanced_batch(
             &scalings,
             VertexArrayKind::Scale,
             &BatchTextures::no_texture(),
             stats,
         );
     }
 
+    fn draw_picture_cache_target(
+        &mut self,
+        target: &PictureCacheTarget,
+        draw_target: DrawTarget,
+        content_origin: DeviceIntPoint,
+        projection: &Transform3D<f32>,
+        render_tasks: &RenderTaskGraph,
+        stats: &mut RendererStats,
+    ) {
+        self.profile_counters.color_targets.inc();
+        let _gm = self.gpu_profile.start_marker("picture cache target");
+        let framebuffer_kind = FramebufferKind::Other;
+
+        {
+            let _timer = self.gpu_profile.start_timer(GPU_TAG_SETUP_TARGET);
+            self.device.bind_draw_target(draw_target);
+            self.device.disable_depth();
+            self.device.enable_depth_write();
+            self.set_blend(false, framebuffer_kind);
+
+            self.device.clear_target(
+                target.clear_color.map(|c| c.to_array()),
+                Some(1.0),
+                None,
+            );
+
+            self.device.disable_depth_write();
+        }
+
+        self.draw_alpha_batch_container(
+            &target.alpha_batch_container,
+            draw_target,
+            content_origin,
+            framebuffer_kind,
+            projection,
+            render_tasks,
+            stats,
+        );
+    }
+
+    /// Draw an alpha batch container into a given draw target. This is used
+    /// by both color and picture cache target kinds.
+    fn draw_alpha_batch_container(
+        &mut self,
+        alpha_batch_container: &AlphaBatchContainer,
+        draw_target: DrawTarget,
+        content_origin: DeviceIntPoint,
+        framebuffer_kind: FramebufferKind,
+        projection: &Transform3D<f32>,
+        render_tasks: &RenderTaskGraph,
+        stats: &mut RendererStats,
+    ) {
+        let uses_scissor = alpha_batch_container.task_scissor_rect.is_some();
+
+        if uses_scissor {
+            self.device.enable_scissor();
+            let scissor_rect = draw_target.build_scissor_rect(
+                alpha_batch_container.task_scissor_rect,
+                content_origin,
+            );
+            self.device.set_scissor_rect(scissor_rect)
+        }
+
+        if !alpha_batch_container.opaque_batches.is_empty()
+            && !self.debug_flags.contains(DebugFlags::DISABLE_OPAQUE_PASS) {
+            let _gl = self.gpu_profile.start_marker("opaque batches");
+            let opaque_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+            self.set_blend(false, framebuffer_kind);
+            //Note: depth equality is needed for split planes
+            self.device.set_depth_func(DepthFunction::LessEqual);
+            self.device.enable_depth();
+            self.device.enable_depth_write();
+
+            // Draw opaque batches front-to-back for maximum
+            // z-buffer efficiency!
+            for batch in alpha_batch_container
+                .opaque_batches
+                .iter()
+                .rev()
+                {
+                    if should_skip_batch(&batch.key.kind, &self.debug_flags) {
+                        continue;
+                    }
+
+                    self.shaders.borrow_mut()
+                        .get(&batch.key, self.debug_flags)
+                        .bind(
+                            &mut self.device, projection,
+                            &mut self.renderer_errors,
+                        );
+
+                    let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
+                    self.draw_instanced_batch(
+                        &batch.instances,
+                        VertexArrayKind::Primitive,
+                        &batch.key.textures,
+                        stats
+                    );
+                }
+
+            self.device.disable_depth_write();
+            self.gpu_profile.finish_sampler(opaque_sampler);
+        }
+
+        if !alpha_batch_container.alpha_batches.is_empty()
+            && !self.debug_flags.contains(DebugFlags::DISABLE_ALPHA_PASS) {
+            let _gl = self.gpu_profile.start_marker("alpha batches");
+            let transparent_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+            self.set_blend(true, framebuffer_kind);
+            let mut prev_blend_mode = BlendMode::None;
+
+            // If the device supports pixel local storage, initialize the PLS buffer for
+            // the transparent pass. This involves reading the current framebuffer value
+            // and storing that in PLS.
+            // TODO(gw): This is quite expensive and relies on framebuffer fetch being
+            //           available. We can probably switch the opaque pass over to use
+            //           PLS too, and remove this pass completely.
+            if self.device.get_capabilities().supports_pixel_local_storage {
+                // TODO(gw): If using PLS, the fixed function blender is disabled. It's possible
+                //           we could take advantage of this by skipping batching on the blend
+                //           mode in these cases.
+                self.init_pixel_local_storage(
+                    alpha_batch_container.task_rect,
+                    projection,
+                    stats,
+                );
+            }
+
+            for batch in &alpha_batch_container.alpha_batches {
+                if should_skip_batch(&batch.key.kind, &self.debug_flags) {
+                    continue;
+                }
+
+                self.shaders.borrow_mut()
+                    .get(&batch.key, self.debug_flags)
+                    .bind(
+                        &mut self.device, projection,
+                        &mut self.renderer_errors,
+                    );
+
+                if batch.key.blend_mode != prev_blend_mode {
+                    match batch.key.blend_mode {
+                        _ if self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) &&
+                            framebuffer_kind == FramebufferKind::Main => {
+                            self.device.set_blend_mode_show_overdraw();
+                        }
+                        BlendMode::None => {
+                            unreachable!("bug: opaque blend in alpha pass");
+                        }
+                        BlendMode::Alpha => {
+                            self.device.set_blend_mode_alpha();
+                        }
+                        BlendMode::PremultipliedAlpha => {
+                            self.device.set_blend_mode_premultiplied_alpha();
+                        }
+                        BlendMode::PremultipliedDestOut => {
+                            self.device.set_blend_mode_premultiplied_dest_out();
+                        }
+                        BlendMode::SubpixelDualSource => {
+                            self.device.set_blend_mode_subpixel_dual_source();
+                        }
+                        BlendMode::SubpixelConstantTextColor(color) => {
+                            self.device.set_blend_mode_subpixel_constant_text_color(color);
+                        }
+                        BlendMode::SubpixelWithBgColor => {
+                            // Using the three pass "component alpha with font smoothing
+                            // background color" rendering technique:
+                            //
+                            // /webrender/doc/text-rendering.md
+                            //
+                            self.device.set_blend_mode_subpixel_with_bg_color_pass0();
+                            self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
+                        }
+                        BlendMode::Advanced(mode) => {
+                            if self.enable_advanced_blend_barriers {
+                                self.device.gl().blend_barrier_khr();
+                            }
+                            self.device.set_blend_mode_advanced(mode);
+                        }
+                    }
+                    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());
+
+                self.draw_instanced_batch(
+                    &batch.instances,
+                    VertexArrayKind::Primitive,
+                    &batch.key.textures,
+                    stats
+                );
+
+                if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+                    self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+                    self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
+
+                    // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
+                    // are all set up from the previous draw_instanced_batch call,
+                    // so just issue a draw call here to avoid re-uploading the
+                    // instances and re-binding textures etc.
+                    self.device
+                        .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+
+                    self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+                    self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
+
+                    self.device
+                        .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+                }
+
+                if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+                    prev_blend_mode = BlendMode::None;
+                }
+            }
+
+            // If the device supports pixel local storage, resolve the PLS values.
+            // This pass reads the final PLS color value, and writes it to a normal
+            // fragment output.
+            if self.device.get_capabilities().supports_pixel_local_storage {
+                self.resolve_pixel_local_storage(
+                    alpha_batch_container.task_rect,
+                    projection,
+                    stats,
+                );
+            }
+
+            self.device.disable_depth();
+            self.set_blend(false, framebuffer_kind);
+            self.gpu_profile.finish_sampler(transparent_sampler);
+        }
+
+        if uses_scissor {
+            self.device.disable_scissor();
+        }
+    }
+
     fn draw_color_target(
         &mut self,
         draw_target: DrawTarget,
         target: &ColorRenderTarget,
         content_origin: DeviceIntPoint,
         clear_color: Option<[f32; 4]>,
         clear_depth: Option<f32>,
         render_tasks: &RenderTaskGraph,
@@ -3490,308 +3739,26 @@ impl Renderer {
 
         self.handle_scaling(
             &target.scalings,
             TextureSource::PrevPassColor,
             projection,
             stats,
         );
 
-        // Small helper fn to iterate a regions list, also invoking the closure
-        // if there are no regions.
-        fn iterate_regions<F>(
-            regions: &[DeviceIntRect],
-            mut f: F,
-        ) where F: FnMut(Option<DeviceIntRect>) {
-            if regions.is_empty() {
-                f(None)
-            } else {
-                for region in regions {
-                    f(Some(*region))
-                }
-            }
-        }
-
-        fn should_skip_batch(kind: &BatchKind, flags: &DebugFlags) -> bool {
-            match kind {
-                BatchKind::TextRun(_) => {
-                    flags.contains(DebugFlags::DISABLE_TEXT_PRIMS)
-                }
-                BatchKind::Brush(BrushBatchKind::RadialGradient) |
-                BatchKind::Brush(BrushBatchKind::LinearGradient) => {
-                    flags.contains(DebugFlags::DISABLE_GRADIENT_PRIMS)
-                }
-                _ => false,
-            }
-        }
-
         for alpha_batch_container in &target.alpha_batch_containers {
-            let uses_scissor = alpha_batch_container.task_scissor_rect.is_some() ||
-                               !alpha_batch_container.regions.is_empty();
-
-            if uses_scissor {
-                self.device.enable_scissor();
-                let scissor_rect = draw_target.build_scissor_rect(
-                    alpha_batch_container.task_scissor_rect,
-                    content_origin,
-                );
-                self.device.set_scissor_rect(scissor_rect)
-            }
-
-            if !alpha_batch_container.opaque_batches.is_empty()
-                    && !self.debug_flags.contains(DebugFlags::DISABLE_OPAQUE_PASS) {
-                let _gl = self.gpu_profile.start_marker("opaque batches");
-                let opaque_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
-                self.set_blend(false, framebuffer_kind);
-                //Note: depth equality is needed for split planes
-                self.device.set_depth_func(DepthFunction::LessEqual);
-                self.device.enable_depth();
-                self.device.enable_depth_write();
-
-                // Draw opaque batches front-to-back for maximum
-                // z-buffer efficiency!
-                for batch in alpha_batch_container
-                    .opaque_batches
-                    .iter()
-                    .rev()
-                {
-                    if should_skip_batch(&batch.key.kind, &self.debug_flags) {
-                        continue;
-                    }
-
-                    self.shaders.borrow_mut()
-                        .get(&batch.key, self.debug_flags)
-                        .bind(
-                            &mut self.device, projection,
-                            &mut self.renderer_errors,
-                        );
-
-                    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),
-                                    content_origin,
-                                );
-                                self.device.set_scissor_rect(scissor_rect);
-                            }
-
-                            self.draw_instanced_batch(
-                                &batch.instances,
-                                VertexArrayKind::Primitive,
-                                &batch.key.textures,
-                                stats
-                            );
-                        }
-                    );
-                }
-
-                self.device.disable_depth_write();
-                self.gpu_profile.finish_sampler(opaque_sampler);
-            }
-
-            if !alpha_batch_container.alpha_batches.is_empty()
-                    && !self.debug_flags.contains(DebugFlags::DISABLE_ALPHA_PASS) {
-                let _gl = self.gpu_profile.start_marker("alpha batches");
-                let transparent_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
-                self.set_blend(true, framebuffer_kind);
-                let mut prev_blend_mode = BlendMode::None;
-
-                // If the device supports pixel local storage, initialize the PLS buffer for
-                // the transparent pass. This involves reading the current framebuffer value
-                // and storing that in PLS.
-                // TODO(gw): This is quite expensive and relies on framebuffer fetch being
-                //           available. We can probably switch the opaque pass over to use
-                //           PLS too, and remove this pass completely.
-                if self.device.get_capabilities().supports_pixel_local_storage {
-                    // TODO(gw): If using PLS, the fixed function blender is disabled. It's possible
-                    //           we could take advantage of this by skipping batching on the blend
-                    //           mode in these cases.
-                    self.init_pixel_local_storage(
-                        alpha_batch_container.task_rect,
-                        projection,
-                        stats,
-                    );
-                }
-
-                for batch in &alpha_batch_container.alpha_batches {
-                    if should_skip_batch(&batch.key.kind, &self.debug_flags) {
-                        continue;
-                    }
-
-                    self.shaders.borrow_mut()
-                        .get(&batch.key, self.debug_flags)
-                        .bind(
-                            &mut self.device, projection,
-                            &mut self.renderer_errors,
-                        );
-
-                    if batch.key.blend_mode != prev_blend_mode {
-                        match batch.key.blend_mode {
-                            _ if self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) &&
-                                    framebuffer_kind == FramebufferKind::Main => {
-                                self.device.set_blend_mode_show_overdraw();
-                            }
-                            BlendMode::None => {
-                                unreachable!("bug: opaque blend in alpha pass");
-                            }
-                            BlendMode::Alpha => {
-                                self.device.set_blend_mode_alpha();
-                            }
-                            BlendMode::PremultipliedAlpha => {
-                                self.device.set_blend_mode_premultiplied_alpha();
-                            }
-                            BlendMode::PremultipliedDestOut => {
-                                self.device.set_blend_mode_premultiplied_dest_out();
-                            }
-                            BlendMode::SubpixelDualSource => {
-                                self.device.set_blend_mode_subpixel_dual_source();
-                            }
-                            BlendMode::SubpixelConstantTextColor(color) => {
-                                self.device.set_blend_mode_subpixel_constant_text_color(color);
-                            }
-                            BlendMode::SubpixelWithBgColor => {
-                                // Using the three pass "component alpha with font smoothing
-                                // background color" rendering technique:
-                                //
-                                // /webrender/doc/text-rendering.md
-                                //
-                                self.device.set_blend_mode_subpixel_with_bg_color_pass0();
-                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
-                            }
-                            BlendMode::Advanced(mode) => {
-                                if self.enable_advanced_blend_barriers {
-                                    self.device.gl().blend_barrier_khr();
-                                }
-                                self.device.set_blend_mode_advanced(mode);
-                            }
-                        }
-                        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),
-                                    content_origin,
-                                );
-                                self.device.set_scissor_rect(scissor_rect);
-                            }
-
-                            self.draw_instanced_batch(
-                                &batch.instances,
-                                VertexArrayKind::Primitive,
-                                &batch.key.textures,
-                                stats
-                            );
-
-                            if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
-                                self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
-                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
-
-                                // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
-                                // are all set up from the previous draw_instanced_batch call,
-                                // so just issue a draw call here to avoid re-uploading the
-                                // instances and re-binding textures etc.
-                                self.device
-                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-
-                                self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
-                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
-
-                                self.device
-                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-                            }
-                        }
-                    );
-
-                    if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
-                        prev_blend_mode = BlendMode::None;
-                    }
-                }
-
-                // If the device supports pixel local storage, resolve the PLS values.
-                // This pass reads the final PLS color value, and writes it to a normal
-                // fragment output.
-                if self.device.get_capabilities().supports_pixel_local_storage {
-                    self.resolve_pixel_local_storage(
-                        alpha_batch_container.task_rect,
-                        projection,
-                        stats,
-                    );
-                }
-
-                self.device.disable_depth();
-                self.set_blend(false, framebuffer_kind);
-                self.gpu_profile.finish_sampler(transparent_sampler);
-            }
-
-            if uses_scissor {
-                self.device.disable_scissor();
-            }
-
-            // At the end of rendering a container, blit across any cache tiles
-            // to the texture cache for use on subsequent frames.
-            if !alpha_batch_container.tile_blits.is_empty() {
-                let _timer = self.gpu_profile.start_timer(GPU_TAG_BLIT);
-
-                for blit in &alpha_batch_container.tile_blits {
-                    let texture = self.texture_resolver
-                        .resolve(&blit.target.texture_id)
-                        .expect("BUG: invalid target texture");
-
-                    let blit_target = DrawTarget::from_texture(
-                        texture,
-                        blit.target.texture_layer as usize,
-                        false,
-                    );
-
-                    let src_rect = draw_target.to_framebuffer_rect(DeviceIntRect::new(
-                        blit.src_offset - content_origin.to_vector(),
-                        blit.size,
-                    ));
-
-                    let target_rect = blit.target.uv_rect.to_i32();
-
-                    let dest_rect = blit_target.to_framebuffer_rect(DeviceIntRect::new(
-                        blit.dest_offset + (target_rect.origin - content_origin),
-                        blit.size,
-                    ));
-
-                    self.device.blit_render_target_invert_y(
-                        draw_target.into(),
-                        src_rect,
-                        blit_target,
-                        dest_rect,
-                    );
-                }
-
-                self.device.bind_draw_target(draw_target);
-            }
+            self.draw_alpha_batch_container(
+                alpha_batch_container,
+                draw_target,
+                content_origin,
+                framebuffer_kind,
+                projection,
+                render_tasks,
+                stats,
+            );
         }
 
         // For any registered image outputs on this render target,
         // get the texture from caller and blit it.
         for output in &target.outputs {
             let handler = self.output_image_handler
                 .as_mut()
                 .expect("Found output image, but no handler set!");
@@ -4493,33 +4460,70 @@ impl Renderer {
                             None,
                             &frame.render_tasks,
                             &projection,
                             frame_id,
                             stats,
                         );
                     }
                 }
-                RenderPassKind::OffScreen { ref mut alpha, ref mut color, ref mut texture_cache } => {
+                RenderPassKind::OffScreen {
+                    ref mut alpha,
+                    ref mut color,
+                    ref mut texture_cache,
+                    ref mut picture_cache,
+                } => {
                     let alpha_tex = self.allocate_target_texture(alpha, &mut frame.profile_counters);
                     let color_tex = self.allocate_target_texture(color, &mut frame.profile_counters);
 
                     // If this frame has already been drawn, then any texture
                     // cache targets have already been updated and can be
                     // skipped this time.
                     if !frame.has_been_rendered {
                         for (&(texture_id, target_index), target) in texture_cache {
                             self.draw_texture_cache_target(
                                 &texture_id,
                                 target_index,
                                 target,
                                 &frame.render_tasks,
                                 stats,
                             );
                         }
+
+                        // Draw picture caching tiles for this pass.
+                        for picture_target in picture_cache {
+                            stats.color_target_count += 1;
+
+                            let texture = self.texture_resolver
+                                .resolve(&picture_target.texture)
+                                .expect("bug");
+                            let draw_target = DrawTarget::from_texture(
+                                texture,
+                                picture_target.layer,
+                                true,
+                            );
+
+                            let projection = Transform3D::ortho(
+                                0.0,
+                                draw_target.dimensions().width as f32,
+                                0.0,
+                                draw_target.dimensions().height as f32,
+                                ORTHO_NEAR_PLANE,
+                                ORTHO_FAR_PLANE,
+                            );
+
+                            self.draw_picture_cache_target(
+                                picture_target,
+                                draw_target,
+                                frame.content_origin,
+                                &projection,
+                                &frame.render_tasks,
+                                stats,
+                            );
+                        }
                     }
 
                     for (target_index, target) in alpha.targets.iter().enumerate() {
                         stats.alpha_target_count += 1;
                         let draw_target = DrawTarget::from_texture(
                             &alpha_tex.as_ref().unwrap().texture,
                             target_index,
                             false,
@@ -5502,16 +5506,17 @@ pub struct RenderResults {
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainTexture {
     data: String,
     size: (DeviceIntSize, i32),
     format: ImageFormat,
     filter: TextureFilter,
+    has_depth: bool,
 }
 
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct PlainRenderer {
     device_size: Option<DeviceIntSize>,
@@ -5611,16 +5616,17 @@ impl Renderer {
                 .unwrap();
         }
 
         PlainTexture {
             data: short_path,
             size: (rect_size, texture.get_layer_count()),
             format: texture.get_format(),
             filter: texture.get_filter(),
+            has_depth: texture.supports_depth(),
         }
     }
 
     #[cfg(feature = "replay")]
     fn load_texture(
         target: TextureTarget,
         plain: &PlainTexture,
         rt_info: Option<RenderTargetInfo>,
@@ -5818,17 +5824,17 @@ impl Renderer {
             for (_id, texture) in self.texture_resolver.texture_cache_map.drain() {
                 self.device.delete_texture(texture);
             }
             for (id, texture) in renderer.textures {
                 info!("\t{}", texture.data);
                 let t = Self::load_texture(
                     TextureTarget::Array,
                     &texture,
-                    Some(RenderTargetInfo { has_depth: false }),
+                    Some(RenderTargetInfo { has_depth: texture.has_depth }),
                     &root,
                     &mut self.device
                 );
                 self.texture_resolver.texture_cache_map.insert(id, t.0);
             }
 
             info!("loading gpu cache");
             if let Some(t) = self.gpu_cache_texture.texture.take() {
@@ -5880,16 +5886,17 @@ impl Renderer {
                     Entry::Vacant(e) => {
                         //TODO: provide a way to query both the layer count and the filter from external images
                         let (layer_count, filter) = (1, TextureFilter::Linear);
                         let plain_tex = PlainTexture {
                             data: e.key().clone(),
                             size: (descriptor.size, layer_count),
                             format: descriptor.format,
                             filter,
+                            has_depth: false,
                         };
                         let t = Self::load_texture(
                             target,
                             &plain_tex,
                             None,
                             &root,
                             &mut self.device
                         );
@@ -5948,8 +5955,21 @@ fn get_vao<'a>(vertex_array_kind: Vertex
         VertexArrayKind::Resolve => &vaos.resolve_vao,
     }
 }
 #[derive(Clone, Copy, PartialEq)]
 enum FramebufferKind {
     Main,
     Other,
 }
+
+fn should_skip_batch(kind: &BatchKind, flags: &DebugFlags) -> bool {
+    match kind {
+        BatchKind::TextRun(_) => {
+            flags.contains(DebugFlags::DISABLE_TEXT_PRIMS)
+        }
+        BatchKind::Brush(BrushBatchKind::RadialGradient) |
+        BatchKind::Brush(BrushBatchKind::LinearGradient) => {
+            flags.contains(DebugFlags::DISABLE_GRADIENT_PRIMS)
+        }
+        _ => false,
+    }
+}
--- a/gfx/wr/webrender/src/spatial_node.rs
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -357,17 +357,18 @@ impl SpatialNode {
                         // Push that new coordinate system and record the new id.
                         let coord_system = {
                             let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
                             let mut cur_transform = transform;
                             if parent_system.should_flatten {
                                 cur_transform.flatten_z_output();
                             }
                             let world_transform = cur_transform.post_mul(&parent_system.world_transform);
-                            info.invertible = world_transform.determinant() != 0.0;
+                            let determinant = world_transform.determinant();
+                            info.invertible = determinant != 0.0 && !determinant.is_nan();
 
                             CoordinateSystem {
                                 transform,
                                 world_transform,
                                 should_flatten: match (info.transform_style, info.kind) {
                                     (TransformStyle::Flat, ReferenceFrameKind::Transform) => true,
                                     (_, _) => false,
                                 },
--- a/gfx/wr/webrender/src/texture_cache.rs
+++ b/gfx/wr/webrender/src/texture_cache.rs
@@ -551,29 +551,30 @@ impl TextureCache {
             //     the same max texture size of 16k. If we do encounter a driver
             //     with the same bug but a lower max texture size, we might need
             //     to rethink our strategy anyway, since a limit below 32MB might
             //     start to introduce performance issues.
             max_texture_layers = max_texture_layers.min(32);
         }
 
         let mut pending_updates = TextureUpdateList::new();
-        let picture_texture = if let Some(tile_size) = picture_tile_size{
+        let picture_texture = if let Some(tile_size) = picture_tile_size {
             let picture_texture = WholeTextureArray {
                 size: tile_size,
-                filter: TextureFilter::Linear,
+                filter: TextureFilter::Nearest,
                 format: PICTURE_TILE_FORMAT,
                 texture_id: CacheTextureId(1),
                 slices: {
                     let num_x = (initial_size.width + tile_size.width - 1) / tile_size.width;
                     let num_y = (initial_size.height + tile_size.height - 1) / tile_size.height;
                     let count = (num_x * num_y).max(1) as usize;
                     info!("Initializing picture texture with {}x{} slices", num_x, num_y);
                     vec![WholeTextureSlice { uv_rect_handle: None }; count]
                 },
+                has_depth: true,
             };
             pending_updates.push_alloc(picture_texture.texture_id, picture_texture.to_info());
             Some(picture_texture)
         } else {
             None
         };
 
         TextureCache {
@@ -1087,16 +1088,17 @@ impl TextureCache {
 
             let info = TextureCacheAllocInfo {
                 width: TEXTURE_REGION_DIMENSIONS,
                 height: TEXTURE_REGION_DIMENSIONS,
                 format: params.descriptor.format,
                 filter: texture_array.filter,
                 layer_count: 1,
                 is_shared_cache: true,
+                has_depth: false,
             };
             self.pending_updates.push_alloc(texture_id, info);
 
             texture_array.texture_id = Some(texture_id);
             texture_array.push_region();
         }
 
         // Do the allocation. This can fail and return None
@@ -1150,16 +1152,17 @@ impl TextureCache {
         // Push a command to allocate device storage of the right size / format.
         let info = TextureCacheAllocInfo {
             width: params.descriptor.size.width,
             height: params.descriptor.size.height,
             format: params.descriptor.format,
             filter: params.filter,
             layer_count: 1,
             is_shared_cache: false,
+            has_depth: false,
         };
         self.pending_updates.push_alloc(texture_id, info);
 
         CacheEntry::new_standalone(
             texture_id,
             self.now,
             params,
         )
@@ -1218,16 +1221,17 @@ impl TextureCache {
             if num_regions < self.max_texture_layers as usize {
                 let info = TextureCacheAllocInfo {
                     width: TEXTURE_REGION_DIMENSIONS,
                     height: TEXTURE_REGION_DIMENSIONS,
                     format: params.descriptor.format,
                     filter: texture_array.filter,
                     layer_count: (num_regions + 1) as i32,
                     is_shared_cache: true,
+                    has_depth: false,
                 };
                 self.pending_updates.push_realloc(texture_array.texture_id.unwrap(), info);
                 texture_array.push_region();
                 true
             } else {
                 false
             }
         };
@@ -1603,27 +1607,29 @@ struct WholeTextureSlice {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct WholeTextureArray {
     size: DeviceIntSize,
     filter: TextureFilter,
     format: ImageFormat,
     texture_id: CacheTextureId,
     slices: Vec<WholeTextureSlice>,
+    has_depth: bool,
 }
 
 impl WholeTextureArray {
     fn to_info(&self) -> TextureCacheAllocInfo {
         TextureCacheAllocInfo {
             width: self.size.width,
             height: self.size.height,
             format: self.format,
             filter: self.filter,
             layer_count: self.slices.len() as i32,
             is_shared_cache: true, //TODO: reconsider
+            has_depth: self.has_depth,
         }
     }
 
     /// Returns the number of GPU bytes consumed by this texture array.
     fn size_in_bytes(&self) -> usize {
         let bpp = self.format.bytes_per_pixel() as usize;
         self.slices.len() * (self.size.width * self.size.height) as usize * bpp
     }
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -4,31 +4,31 @@
 
 use api::{ColorF, BorderStyle, MixBlendMode, PipelineId, PremultipliedColorF};
 use api::{DocumentLayer, FilterData, ImageFormat, LineOrientation};
 use api::units::*;
 #[cfg(feature = "pathfinder")]
 use api::FontRenderMode;
 use crate::batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image, BatchBuilder};
 use crate::clip::ClipStore;
-use crate::clip_scroll_tree::{ClipScrollTree};
+use crate::clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX};
 use crate::debug_render::DebugItem;
 use crate::device::{Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use crate::frame_builder::FrameGlobalResources;
 use crate::gpu_cache::{GpuCache};
 use crate::gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
 use crate::gpu_types::{TransformData, TransformPalette, ZBufferIdGenerator};
 use crate::internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource, Filter};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use crate::picture::{RecordedDirtyRegion, SurfaceInfo};
 use crate::prim_store::gradient::GRADIENT_FP_STOPS;
-use crate::prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
+use crate::prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer, PrimitiveVisibilityMask};
 use crate::profiler::FrameProfileCounters;
 use crate::render_backend::{DataStores, FrameId};
 use crate::render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind};
 use crate::render_task::{BlurTask, ClearMode, GlyphTask, RenderTaskLocation, RenderTaskGraph, ScalingTask};
 use crate::resource_cache::ResourceCache;
 use std::{cmp, usize, f32, i32, mem};
 use crate::texture_allocator::{ArrayAllocationTracker, FreeRectSlice};
 
@@ -117,17 +117,16 @@ pub trait RenderTarget {
         gpu_cache: &mut GpuCache,
         render_tasks: &RenderTaskGraph,
         clip_store: &ClipStore,
         transforms: &mut TransformPalette,
         deferred_resolves: &mut Vec<DeferredResolve>,
     );
 
     fn needs_depth(&self) -> bool;
-    fn must_be_drawn(&self) -> bool;
 
     fn used_rect(&self) -> DeviceIntRect;
     fn add_used(&mut self, rect: DeviceIntRect);
 }
 
 /// A tag used to identify the output format of a `RenderTarget`.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -269,20 +268,16 @@ impl<T: RenderTarget> RenderTargetList<T
 
         (RenderTargetIndex(free_rect_slice.0 as usize), origin)
     }
 
     pub fn needs_depth(&self) -> bool {
         self.targets.iter().any(|target| target.needs_depth())
     }
 
-    pub fn must_be_drawn(&self) -> bool {
-        self.targets.iter().any(|target| target.must_be_drawn())
-    }
-
     pub fn check_ready(&self, t: &Texture) {
         let dimensions = t.get_dimensions();
         assert!(dimensions.width >= self.max_dynamic_size.width);
         assert!(dimensions.height >= self.max_dynamic_size.height);
         assert_eq!(t.get_format(), self.format);
         assert_eq!(t.get_layer_count() as usize, self.targets.len());
         assert!(t.supports_depth() >= self.needs_depth());
     }
@@ -401,17 +396,17 @@ impl RenderTarget for ColorRenderTarget 
         ctx: &mut RenderTargetContext,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskGraph,
         deferred_resolves: &mut Vec<DeferredResolve>,
         prim_headers: &mut PrimitiveHeaders,
         transforms: &mut TransformPalette,
         z_generator: &mut ZBufferIdGenerator,
     ) {
-        let mut merged_batches = AlphaBatchContainer::new(None, Vec::new());
+        let mut merged_batches = AlphaBatchContainer::new(None);
 
         for task_id in &self.alpha_tasks {
             let task = &render_tasks[*task_id];
 
             match task.clear_mode {
                 ClearMode::One |
                 ClearMode::Zero => {
                     panic!("bug: invalid clear mode for color task");
@@ -419,16 +414,27 @@ impl RenderTarget for ColorRenderTarget 
                 ClearMode::DontCare |
                 ClearMode::Transparent => {}
             }
 
             match task.kind {
                 RenderTaskKind::Picture(ref pic_task) => {
                     let pic = &ctx.prim_store.pictures[pic_task.pic_index.0];
 
+                    let raster_spatial_node_index = match pic.raster_config {
+                        Some(ref raster_config) => {
+                            let surface = &ctx.surfaces[raster_config.surface_index.0];
+                            surface.raster_spatial_node_index
+                        }
+                        None => {
+                            // This must be the main framebuffer
+                            ROOT_SPATIAL_NODE_INDEX
+                        }
+                    };
+
                     let (target_rect, _) = task.get_target_rect();
 
                     let scissor_rect = if pic_task.can_merge {
                         None
                     } else {
                         Some(target_rect)
                     };
 
@@ -438,43 +444,46 @@ impl RenderTarget for ColorRenderTarget 
                     //           AlphaBatchList types will be collapsed into one, which
                     //           should simplify coming up with better type names.
                     let alpha_batch_builder = AlphaBatchBuilder::new(
                         self.screen_size,
                         ctx.break_advanced_blend_batches,
                         ctx.batch_lookback_count,
                         *task_id,
                         render_tasks.get_task_address(*task_id),
+                        PrimitiveVisibilityMask::all(),
                     );
 
                     let mut batch_builder = BatchBuilder::new(
-                        alpha_batch_builder,
+                        vec![alpha_batch_builder],
                     );
 
                     batch_builder.add_pic_to_batch(
                         pic,
                         ctx,
                         gpu_cache,
                         render_tasks,
                         deferred_resolves,
                         prim_headers,
                         transforms,
-                        pic_task.root_spatial_node_index,
+                        raster_spatial_node_index,
                         pic_task.surface_spatial_node_index,
                         z_generator,
                     );
 
-                    let alpha_batch_builder = batch_builder.finalize();
+                    let alpha_batch_builders = batch_builder.finalize();
 
-                    alpha_batch_builder.build(
-                        &mut self.alpha_batch_containers,
-                        &mut merged_batches,
-                        target_rect,
-                        scissor_rect,
-                    );
+                    for batcher in alpha_batch_builders {
+                        batcher.build(
+                            &mut self.alpha_batch_containers,
+                            &mut merged_batches,
+                            target_rect,
+                            scissor_rect,
+                        );
+                    }
                 }
                 _ => {
                     unreachable!();
                 }
             }
         }
 
         if !merged_batches.is_empty() {
@@ -588,22 +597,16 @@ impl RenderTarget for ColorRenderTarget 
                     }
                 }
             }
             #[cfg(test)]
             RenderTaskKind::Test(..) => {}
         }
     }
 
-    fn must_be_drawn(&self) -> bool {
-        self.alpha_batch_containers.iter().any(|ab| {
-            !ab.tile_blits.is_empty()
-        })
-    }
-
     fn needs_depth(&self) -> bool {
         self.alpha_batch_containers.iter().any(|ab| {
             !ab.opaque_batches.is_empty()
         })
     }
 
     fn used_rect(&self) -> DeviceIntRect {
         self.used_rect
@@ -631,17 +634,17 @@ pub struct AlphaRenderTarget {
     // Track the used rect of the render target, so that
     // we can set a scissor rect and only clear to the
     // used portion of the target as an optimization.
     pub used_rect: DeviceIntRect,
 }
 
 impl RenderTarget for AlphaRenderTarget {
     fn new(
-        _screen_size: DeviceIntSize,
+        _: DeviceIntSize,
         gpu_supports_fast_clears: bool,
     ) -> Self {
         AlphaRenderTarget {
             clip_batcher: ClipBatcher::new(gpu_supports_fast_clears),
             vertical_blurs: Vec::new(),
             horizontal_blurs: Vec::new(),
             scalings: Vec::new(),
             zero_clears: Vec::new(),
@@ -745,31 +748,36 @@ impl RenderTarget for AlphaRenderTarget 
             RenderTaskKind::Test(..) => {}
         }
     }
 
     fn needs_depth(&self) -> bool {
         false
     }
 
-    fn must_be_drawn(&self) -> bool {
-        false
-    }
-
     fn used_rect(&self) -> DeviceIntRect {
         self.used_rect
     }
 
     fn add_used(&mut self, rect: DeviceIntRect) {
         self.used_rect = self.used_rect.union(&rect);
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheTarget {
+    pub texture: TextureSource,
+    pub layer: usize,
+    pub alpha_batch_container: AlphaBatchContainer,
+    pub clear_color: Option<ColorF>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct TextureCacheRenderTarget {
     pub target_kind: RenderTargetKind,
     pub horizontal_blurs: Vec<BlurInstance>,
     pub blits: Vec<BlitJob>,
     pub glyphs: Vec<GlyphJob>,
     pub border_segments_complex: Vec<BorderInstance>,
     pub border_segments_solid: Vec<BorderInstance>,
     pub clears: Vec<DeviceIntRect>,
@@ -921,16 +929,17 @@ pub enum RenderPassKind {
     MainFramebuffer {
         main_target: ColorRenderTarget,
     },
     /// An intermediate pass, where we may have multiple targets.
     OffScreen {
         alpha: RenderTargetList<AlphaRenderTarget>,
         color: RenderTargetList<ColorRenderTarget>,
         texture_cache: FastHashMap<(CacheTextureId, usize), TextureCacheRenderTarget>,
+        picture_cache: Vec<PictureCacheTarget>,
     },
 }
 
 /// A render pass represents a set of rendering operations that don't depend on one
 /// another.
 ///
 /// A render pass can have several render targets if there wasn't enough space in one
 /// target to do all of the rendering for that pass. See `RenderTargetList`.
@@ -938,31 +947,34 @@ pub enum RenderPassKind {
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderPass {
     /// The kind of pass, as well as the set of targets associated with that
     /// kind of pass.
     pub kind: RenderPassKind,
     /// The set of tasks to be performed in this pass, as indices into the
     /// `RenderTaskGraph`.
     pub tasks: Vec<RenderTaskId>,
+    /// Screen size in device pixels - used for opaque alpha batch break threshold.
+    screen_size: DeviceIntSize,
 }
 
 impl RenderPass {
     /// Creates a pass for the main framebuffer. There is only one of these, and
     /// it is always the last pass.
     pub fn new_main_framebuffer(
         screen_size: DeviceIntSize,
         gpu_supports_fast_clears: bool,
     ) -> Self {
         let main_target = ColorRenderTarget::new(screen_size, gpu_supports_fast_clears);
         RenderPass {
             kind: RenderPassKind::MainFramebuffer {
                 main_target,
             },
             tasks: vec![],
+            screen_size,
         }
     }
 
     /// Creates an intermediate off-screen pass.
     pub fn new_off_screen(
         screen_size: DeviceIntSize,
         gpu_supports_fast_clears: bool,
     ) -> Self {
@@ -974,18 +986,20 @@ impl RenderPass {
                     gpu_supports_fast_clears,
                 ),
                 alpha: RenderTargetList::new(
                     screen_size,
                     ImageFormat::R8,
                     gpu_supports_fast_clears,
                 ),
                 texture_cache: FastHashMap::default(),
+                picture_cache: Vec::new(),
             },
             tasks: vec![],
+            screen_size,
         }
     }
 
     /// Adds a task to this pass.
     pub fn add_render_task(
         &mut self,
         task_id: RenderTaskId,
         size: DeviceIntSize,
@@ -1047,17 +1061,22 @@ impl RenderPass {
                     gpu_cache,
                     render_tasks,
                     deferred_resolves,
                     prim_headers,
                     transforms,
                     z_generator,
                 );
             }
-            RenderPassKind::OffScreen { ref mut color, ref mut alpha, ref mut texture_cache } => {
+            RenderPassKind::OffScreen {
+                ref mut color,
+                ref mut alpha,
+                ref mut texture_cache,
+                ref mut picture_cache,
+            } => {
                 let saved_color = if self.tasks.iter().any(|&task_id| {
                     let t = &render_tasks[task_id];
                     t.target_kind() == RenderTargetKind::Color && t.saved_index.is_some()
                 }) {
                     Some(render_tasks.save_target())
                 } else {
                     None
                 };
@@ -1065,16 +1084,21 @@ impl RenderPass {
                     let t = &render_tasks[task_id];
                     t.target_kind() == RenderTargetKind::Alpha && t.saved_index.is_some()
                 }) {
                     Some(render_tasks.save_target())
                 } else {
                     None
                 };
 
+                // Collect a list of picture cache tasks, keyed by picture index.
+                // This allows us to only walk that picture root once, adding the
+                // primitives to all relevant batches at the same time.
+                let mut picture_cache_tasks = FastHashMap::default();
+
                 // Step through each task, adding to batches as appropriate.
                 for &task_id in &self.tasks {
                     let (target_kind, texture_target, layer) = {
                         let task = &mut render_tasks[task_id];
                         let target_kind = task.target_kind();
 
                         // Find a target to assign this task to, or create a new
                         // one if required.
@@ -1088,16 +1112,35 @@ impl RenderPass {
                             RenderTaskLocation::Dynamic(ref mut origin, size) => {
                                 let (target_index, alloc_origin) =  match target_kind {
                                     RenderTargetKind::Color => color.allocate(size),
                                     RenderTargetKind::Alpha => alpha.allocate(size),
                                 };
                                 *origin = Some((alloc_origin, target_index));
                                 (None, target_index.0)
                             }
+                            RenderTaskLocation::PictureCache { .. } => {
+                                // For picture cache tiles, just store them in the map
+                                // of picture cache tasks, to be handled below.
+                                let pic_index = match task.kind {
+                                    RenderTaskKind::Picture(ref info) => {
+                                        info.pic_index
+                                    }
+                                    _ => {
+                                        unreachable!();
+                                    }
+                                };
+
+                                picture_cache_tasks
+                                    .entry(pic_index)
+                                    .or_insert_with(Vec::new)
+                                    .push(task_id);
+
+                                continue;
+                            }
                         };
 
                         // Replace the pending saved index with a real one
                         if let Some(index) = task.saved_index {
                             assert_eq!(index, SavedTargetIndex::PENDING);
                             task.saved_index = match target_kind {
                                 RenderTargetKind::Color => saved_color,
                                 RenderTargetKind::Alpha => saved_alpha,
@@ -1117,34 +1160,148 @@ impl RenderPass {
                                 .entry((texture_target, layer))
                                 .or_insert_with(||
                                     TextureCacheRenderTarget::new(target_kind)
                                 );
                             texture.add_task(task_id, render_tasks);
                         }
                         None => {
                             match target_kind {
-                                RenderTargetKind::Color => color.targets[layer].add_task(
-                                    task_id,
-                                    ctx,
-                                    gpu_cache,
-                                    render_tasks,
-                                    clip_store,
-                                    transforms,
-                                    deferred_resolves,
-                                ),
-                                RenderTargetKind::Alpha => alpha.targets[layer].add_task(
-                                    task_id,
-                                    ctx,
-                                    gpu_cache,
-                                    render_tasks,
-                                    clip_store,
-                                    transforms,
-                                    deferred_resolves,
-                                ),
+                                RenderTargetKind::Color => {
+                                    color.targets[layer].add_task(
+                                        task_id,
+                                        ctx,
+                                        gpu_cache,
+                                        render_tasks,
+                                        clip_store,
+                                        transforms,
+                                        deferred_resolves,
+                                    )
+                                }
+                                RenderTargetKind::Alpha => {
+                                    alpha.targets[layer].add_task(
+                                        task_id,
+                                        ctx,
+                                        gpu_cache,
+                                        render_tasks,
+                                        clip_store,
+                                        transforms,
+                                        deferred_resolves,
+                                    )
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // For each picture in this pass that has picture cache tiles, create
+                // a batcher per task, and then build batches for each of the tasks
+                // at the same time.
+                for (pic_index, task_ids) in picture_cache_tasks {
+                    let pic = &ctx.prim_store.pictures[pic_index.0];
+                    let tile_cache = pic.tile_cache.as_ref().expect("bug");
+
+                    // Extract raster/surface spatial nodes for this surface.
+                    let (root_spatial_node_index, surface_spatial_node_index) = match pic.raster_config {
+                        Some(ref rc) => {
+                            let surface = &ctx.surfaces[rc.surface_index.0];
+                            (surface.raster_spatial_node_index, surface.surface_spatial_node_index)
+                        }
+                        None => {
+                            unreachable!();
+                        }
+                    };
+
+                    // Determine the clear color for this picture cache.
+                    // If the entire tile cache is opaque, we can skip clear completely.
+                    // If it's the first layer, clear it to white to allow subpixel AA on that
+                    // 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 {
+                        Some(ColorF::WHITE)
+                    } else {
+                        Some(ColorF::TRANSPARENT)
+                    };
+
+                    // 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!(),
+                        };
+                        batchers.push(AlphaBatchBuilder::new(
+                            self.screen_size,
+                            ctx.break_advanced_blend_batches,
+                            ctx.batch_lookback_count,
+                            task_id,
+                            render_tasks.get_task_address(task_id),
+                            vis_mask,
+                        ));
+                    }
+
+                    // Run the batch creation code for this picture, adding items to
+                    // all relevant per-task batchers.
+                    let mut batch_builder = BatchBuilder::new(batchers);
+                    batch_builder.add_pic_to_batch(
+                        pic,
+                        ctx,
+                        gpu_cache,
+                        render_tasks,
+                        deferred_resolves,
+                        prim_headers,
+                        transforms,
+                        root_spatial_node_index,
+                        surface_spatial_node_index,
+                        z_generator,
+                    );
+
+                    // Create picture cache targets, one per render task, and assign
+                    // the correct batcher to them.
+                    let batchers = batch_builder.finalize();
+                    for (task_id, batcher) in task_ids.into_iter().zip(batchers.into_iter()) {
+                        let task = &render_tasks[task_id];
+                        let (target_rect, _) = task.get_target_rect();
+
+                        match task.location {
+                            RenderTaskLocation::PictureCache { texture, layer, .. } => {
+                                // TODO(gw): The interface here is a bit untidy since it's
+                                //           designed to support batch merging, which isn't
+                                //           relevant for picture cache targets. We
+                                //           can restructure / tidy this up a bit.
+                                let mut batch_containers = Vec::new();
+                                let mut alpha_batch_container = AlphaBatchContainer::new(None);
+                                batcher.build(
+                                    &mut batch_containers,
+                                    &mut alpha_batch_container,
+                                    target_rect,
+                                    None,
+                                );
+                                debug_assert!(batch_containers.is_empty());
+
+                                let target = PictureCacheTarget {
+                                    texture,
+                                    layer: layer as usize,
+                                    clear_color,
+                                    alpha_batch_container,
+                                };
+
+                                picture_cache.push(target);
+                            }
+                            _ => {
+                                unreachable!()
                             }
                         }
                     }
                 }
 
                 color.build(
                     ctx,
                     gpu_cache,
--- a/gfx/wr/webrender/src/util.rs
+++ b/gfx/wr/webrender/src/util.rs
@@ -1055,16 +1055,25 @@ impl<T> ComparableVec<T> where T: Partia
             self.is_same = false;
             self.items.push(item);
         }
 
         // Increment where the next item will be pushed.
         self.current_index += 1;
     }
 
+    #[allow(dead_code)]
+    pub fn dump(&self, tag: &str) {
+        println!("{}", tag);
+        let items = self.items();
+        for (i, item) in items.iter().enumerate() {
+            println!("{}/{}: {:?}", i, items.len(), item);
+        }
+    }
+
     /// Return true if the contents of the vec are the same as the previous time.
     pub fn is_valid(&self) -> bool {
         self.is_same && self.prev_len == self.current_index
     }
 }
 
 /// Arc wrapper to support measurement via MallocSizeOf.
 ///
--- a/gfx/wr/wrench/reftests/blend/reftest.list
+++ b/gfx/wr/wrench/reftests/blend/reftest.list
@@ -1,28 +1,28 @@
 # Some tests in this file skipped on debug Android because of panic,
 # GL error 502 at blit_framebuffer (on emulator) or GL error 502 at
 # draw_elements_instanced (on a Pixel2 device). These are marked with
 # skip_on(android) or skip_on(android,debug). Additionally, the ones
 # marked skip_on(android) fail in opt builds on both emulator and device.
 
 skip_on(android,debug) == multiply.yaml multiply-ref.yaml
 skip_on(android) == multiply-2.yaml multiply-2-ref.yaml
-skip_on(android) == color_targets(3) alpha_targets(0) multiply-3.yaml multiply-2-ref.yaml
+skip_on(android) == color_targets(4) alpha_targets(0) multiply-3.yaml multiply-2-ref.yaml
 skip_on(android) == difference.yaml difference-ref.yaml
 skip_on(android) fuzzy(1,30000) == difference-transparent.yaml difference-transparent-ref.yaml
 skip_on(android) == darken.yaml darken-ref.yaml
 skip_on(android) == lighten.yaml lighten-ref.yaml
 
 skip_on(android,debug) skip_on(android,device,release) == repeated-difference.yaml repeated-difference-ref.yaml  # Fails on a Pixel2 in release mode
 
 == isolated.yaml isolated-ref.yaml
 skip_on(android) == isolated-2.yaml isolated-2-ref.yaml
 == isolated-with-filter.yaml isolated-ref.yaml
-== isolated-premultiplied.yaml blank.yaml
+skip_on(android,emulator) == isolated-premultiplied.yaml blank.yaml
 == isolated-premultiplied-2.yaml isolated-premultiplied-2-ref.yaml
 
 == 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
index 53f48e85bcb68889818f9d8b13b756a80da31647..7ab8c3df6017fe99bf526cdc506139e6e6e687a4
GIT binary patch
literal 1051
zc%17D@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>f%%cAi(^Q|oVQmRgI;+Ev|W7t
z+kT68y!mRqAKp%BlV;8d_EMbi&Qo-L{`>lWcj_A4@7=qf-?-=QJjQZ6CQYSA>-mi$
z6Azf*X9lwW-(e4OVXC*~ThhVeq~NL&69Z(maR0WQpd_UCYNtb&$BqThfeIfcU1JS$
zIn=s_A1K{@L(D^vQ+xwRk=pktpf;a3Ux7+zT(N7^X*ra_!gO#S&<cZze}Os^g>r$G
zCNjMRkp;ORk%YG(GN2YD;_w$l8q|VB6u{~Qj<bBa?wBok`L^NzP}V~dEKH3K3IZIX
z6mF^@LgB0LUfVC_CxYBm1vvP4m(<vlU8z-67Up1SXAiQQUwWZba<MQ-?E{g<Devn|
zujC3E1NAQFTykgsC5P#%XPguS=4eg$^y8|mY5V~nu-O)|3QwPxmt82a*enQC%CqE2
zdHjVO17jf|hj+=Jf1SLucv8c_u9#yr;nSVvVK-_OjDfZ+=U-y;|D^gGtDc*jEKQ0`
zny>9--<Wl111&tv5_Bz|alPfT{@%tZx#0)P*j+Z+PT2I<>&CKTmG!Jaal56iy_Pun
zx^c>N^#fLOm2Rp}*mPc`BCg%z8~2ho=A82`TTB9`mEV5N_f30J{yUudC#;w);yK&F
zYxc?yo7pCA;a;+3Hc$Us8PD4eUbkNs<RACA#U6Akzu|pZ*Ajn)rRT+dY;jfrrn$dn
z2iwJ!UrKwt+|6s3!?UzrVd;Oj9ksXf_iM`vdswDvhi^BU^q479X7^m}D8Z8-8+FbV
za;<xG(&L=)iAOQZ6Vv=%iuO6|vwDAW#!>x=bF3#E{po+=gp*1#%O;8EOM0HQdmQ6>
zvf9x9>`dE^lsgV$$@AxJ{u!_Gn0?cX$9fjc`V-Fxp9ueaS8(rN>63FBb)M}tuG-;h
iRIbqd^Zf+3e~g@^kHQWpajphtdInEdKbLh*2~7a!KWaJv
index d817a791be98e264417c879ad5c9e14803e3886b..d8ef6f37bca03bf9b4e0afa9affd35c4f01acf51
GIT binary patch
literal 21390
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