Bug 1520114 - Fix a number of picture caching invalidation issues. r=kvark
authorGlenn Watson <github@intuitionlibrary.com>
Tue, 15 Jan 2019 19:31:05 +0000
changeset 513953 887c1386b62c538508f90bc8342f0c6032aa6c82
parent 513952 cac9133e55727437d1c10e17bc8cc1423c5992eb
child 513954 cbc854890045817aab09f63fa072087a810c466e
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskvark
bugs1520114
milestone66.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 1520114 - Fix a number of picture caching invalidation issues. r=kvark Differential Revision: https://phabricator.services.mozilla.com/D16528
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/intern.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/borders.rs
gfx/wr/webrender/src/prim_store/gradient.rs
gfx/wr/webrender/src/prim_store/image.rs
gfx/wr/webrender/src/prim_store/line_dec.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/prim_store/picture.rs
gfx/wr/webrender/src/prim_store/text_run.rs
gfx/wr/webrender/src/render_backend.rs
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -16,17 +16,17 @@ use app_units::Au;
 use clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
 use hit_test::{HitTestingItem, HitTestingRun};
 use image::simplify_repeated_primitive;
 use intern::{Handle, Internable, InternDebug};
 use internal_types::{FastHashMap, FastHashSet};
-use picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PrimitiveList};
+use picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PrimitiveList, TileCache};
 use prim_store::{PrimitiveInstance, PrimitiveKeyKind, PrimitiveSceneData};
 use prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
 use prim_store::{PrimitiveStoreStats, ScrollNodeAndClipChain, PictureIndex};
 use prim_store::{register_prim_chase_id, get_line_decoration_sizes};
 use prim_store::borders::{ImageBorder, NormalBorderPrim};
 use prim_store::gradient::{GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams};
 use prim_store::image::{Image, YuvImage};
 use prim_store::line_dec::{LineDecoration, LineDecorationCacheKey};
@@ -328,48 +328,55 @@ impl<'a> DisplayListFlattener<'a> {
             &self.resources,
         );
 
         // 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(),
-            LayoutRect::max_rect(),
             Picture {
                 composite_mode_key: PictureCompositeKey::Identity,
             },
         );
 
         let pic_data_handle = self.resources
             .picture_interner
             .intern(&pic_key, || {
                 PrimitiveSceneData {
-                    prim_relative_clip_rect: LayoutRect::max_rect(),
                     prim_size: LayoutSize::zero(),
                     is_backface_visible: true,
                 }
             }
         );
 
+        let tile_cache = TileCache::new(
+            main_scroll_root,
+            &prim_list.prim_instances,
+            *self.pipeline_clip_chain_stack.last().unwrap(),
+            &self.prim_store.pictures,
+        );
+
         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) }),
             Picture3DContext::Out,
             self.scene.root_pipeline_id.unwrap(),
             None,
             true,
             RasterSpace::Screen,
             prim_list,
             main_scroll_root,
             LayoutRect::max_rect(),
             &self.clip_store,
+            Some(tile_cache),
         ));
 
         let instance = PrimitiveInstance::new(
             LayoutPoint::zero(),
+            LayoutRect::max_rect(),
             PrimitiveInstanceKind::Picture {
                 data_handle: pic_data_handle,
                 pic_index: PictureIndex(pic_index)
             },
             ClipChainId::NONE,
             main_scroll_root,
         );
 
@@ -1048,40 +1055,35 @@ impl<'a> DisplayListFlattener<'a> {
         spatial_node_index: SpatialNodeIndex,
         prim: P,
     ) -> PrimitiveInstance
     where
         P: Internable<InternData=PrimitiveSceneData>,
         P::Source: AsInstanceKind<Handle<P::Marker>> + InternDebug,
         DocumentResources: InternerMut<P>,
     {
-        let offset = info.rect.origin.to_vector();
-        let prim_relative_clip_rect = info.clip_rect
-            .translate(&-offset)
-            .into();
-
         // Build a primitive key.
-        let prim_key = prim.build_key(info, prim_relative_clip_rect);
+        let prim_key = prim.build_key(info);
 
         let interner = self.resources.interner_mut();
         let prim_data_handle =
             interner
             .intern(&prim_key, || {
                 PrimitiveSceneData {
-                    prim_relative_clip_rect,
                     prim_size: info.rect.size,
                     is_backface_visible: info.is_backface_visible,
                 }
             });
 
         let instance_kind = prim_key.as_instance_kind(prim_data_handle,
                                                       &mut self.prim_store);
 
         PrimitiveInstance::new(
             info.rect.origin,
+            info.clip_rect,
             instance_kind,
             clip_chain_id,
             spatial_node_index,
         )
     }
 
     pub fn add_primitive_to_hit_testing_list(
         &mut self,
@@ -1383,16 +1385,17 @@ impl<'a> DisplayListFlattener<'a> {
                 stacking_context.requested_raster_space,
                 PrimitiveList::new(
                     stacking_context.primitives,
                     &self.resources,
                 ),
                 stacking_context.spatial_node_index,
                 max_clip,
                 &self.clip_store,
+                None,
             ))
         );
 
         // Create a chain of pictures based on presence of filters,
         // mix-blend-mode and/or 3d rendering context containers.
 
         let mut current_pic_index = leaf_pic_index;
         let mut cur_instance = create_prim_instance(
@@ -1429,16 +1432,17 @@ impl<'a> DisplayListFlattener<'a> {
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         prims,
                         &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
+                    None,
                 ))
             );
 
             cur_instance = create_prim_instance(
                 current_pic_index,
                 PictureCompositeKey::Identity,
                 stacking_context.is_backface_visible,
                 stacking_context.clip_chain_id,
@@ -1463,16 +1467,17 @@ impl<'a> DisplayListFlattener<'a> {
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
                         &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
+                    None,
                 ))
             );
 
             current_pic_index = filter_pic_index;
             cur_instance = create_prim_instance(
                 current_pic_index,
                 composite_mode.into(),
                 stacking_context.is_backface_visible,
@@ -1505,16 +1510,17 @@ impl<'a> DisplayListFlattener<'a> {
                     stacking_context.requested_raster_space,
                     PrimitiveList::new(
                         vec![cur_instance.clone()],
                         &self.resources,
                     ),
                     stacking_context.spatial_node_index,
                     max_clip,
                     &self.clip_store,
+                    None,
                 ))
             );
 
             current_pic_index = blend_pic_index;
             cur_instance = create_prim_instance(
                 blend_pic_index,
                 composite_mode.into(),
                 stacking_context.is_backface_visible,
@@ -1840,39 +1846,39 @@ impl<'a> DisplayListFlattener<'a> {
                                 raster_space,
                                 PrimitiveList::new(
                                     prims,
                                     &self.resources,
                                 ),
                                 pending_shadow.clip_and_scroll.spatial_node_index,
                                 max_clip,
                                 &self.clip_store,
+                                None,
                             ))
                         );
 
                         let shadow_pic_key = PictureKey::new(
                             true,
                             LayoutSize::zero(),
-                            LayoutRect::max_rect(),
                             Picture { composite_mode_key },
                         );
 
                         let shadow_prim_data_handle = self.resources
                             .picture_interner
                             .intern(&shadow_pic_key, || {
                                 PrimitiveSceneData {
-                                    prim_relative_clip_rect: LayoutRect::max_rect(),
                                     prim_size: LayoutSize::zero(),
                                     is_backface_visible: true,
                                 }
                             }
                         );
 
                         let shadow_prim_instance = PrimitiveInstance::new(
                             LayoutPoint::zero(),
+                            LayoutRect::max_rect(),
                             PrimitiveInstanceKind::Picture {
                                 data_handle: shadow_prim_data_handle,
                                 pic_index: shadow_pic_index
                             },
                             pending_shadow.clip_and_scroll.clip_chain_id,
                             pending_shadow.clip_and_scroll.spatial_node_index,
                         );
 
@@ -2612,16 +2618,17 @@ impl FlattenedStackingContext {
                 self.requested_raster_space,
                 PrimitiveList::new(
                     mem::replace(&mut self.primitives, Vec::new()),
                     resources,
                 ),
                 self.spatial_node_index,
                 LayoutRect::max_rect(),
                 clip_store,
+                None,
             ))
         );
 
         let prim_instance = create_prim_instance(
             pic_index,
             PictureCompositeKey::Identity,
             self.is_backface_visible,
             self.clip_chain_id,
@@ -2694,33 +2701,32 @@ fn create_prim_instance(
     is_backface_visible: bool,
     clip_chain_id: ClipChainId,
     spatial_node_index: SpatialNodeIndex,
     resources: &mut DocumentResources,
 ) -> PrimitiveInstance {
     let pic_key = PictureKey::new(
         is_backface_visible,
         LayoutSize::zero(),
-        LayoutRect::max_rect(),
         Picture { composite_mode_key },
     );
 
     let data_handle = resources
         .picture_interner
         .intern(&pic_key, || {
             PrimitiveSceneData {
-                prim_relative_clip_rect: LayoutRect::max_rect(),
                 prim_size: LayoutSize::zero(),
                 is_backface_visible,
             }
         }
     );
 
     PrimitiveInstance::new(
         LayoutPoint::zero(),
+        LayoutRect::max_rect(),
         PrimitiveInstanceKind::Picture {
             data_handle,
             pic_index,
         },
         clip_chain_id,
         spatial_node_index,
     )
 }
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -8,17 +8,17 @@ use api::{LayoutPoint, LayoutRect, Layou
 use clip::{ClipDataStore, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::GpuCache;
 use gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
 use hit_test::{HitTester, HitTestingRun};
 use internal_types::{FastHashMap, PlaneSplitter};
 use picture::{PictureSurface, PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex};
-use picture::{TileCacheUpdateState, RetainedTiles};
+use picture::{RetainedTiles, TileCache};
 use prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
 #[cfg(feature = "replay")]
 use prim_store::{PrimitiveStoreStats};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use render_backend::{FrameResources, FrameStamp};
 use render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskTree};
 use resource_cache::{ResourceCache};
 use scene::{ScenePipeline, SceneProperties};
@@ -70,23 +70,28 @@ pub struct FrameBuilder {
     pub config: FrameBuilderConfig,
 }
 
 pub struct FrameVisibilityContext<'a> {
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub screen_world_rect: WorldRect,
     pub device_pixel_scale: DevicePixelScale,
     pub surfaces: &'a [SurfaceInfo],
+    pub debug_flags: DebugFlags,
+    pub scene_properties: &'a SceneProperties,
 }
 
 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<TileCache>,
+    pub retained_tiles: &'a mut RetainedTiles,
+    pub resources: &'a mut FrameResources,
 }
 
 pub struct FrameBuildingContext<'a> {
     pub device_pixel_scale: DevicePixelScale,
     pub scene_properties: &'a SceneProperties,
     pub pipelines: &'a FastHashMap<PipelineId, Arc<ScenePipeline>>,
     pub screen_world_rect: WorldRect,
     pub clip_scroll_tree: &'a ClipScrollTree,
@@ -203,30 +208,32 @@ 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,
     ) {
         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
         // avoid this, if there are still pending tiles, include them in
         // the retained tiles passed to the next frame builder.
-        retained_tiles.tiles.extend(self.pending_retained_tiles.tiles);
+        retained_tiles.merge(self.pending_retained_tiles);
     }
 
     /// 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,
         clip_scroll_tree: &ClipScrollTree,
         pipelines: &FastHashMap<PipelineId, Arc<ScenePipeline>>,
@@ -297,54 +304,41 @@ impl FrameBuilder {
             self.root_pic_index,
             &mut pic_update_state,
             &frame_context,
             gpu_cache,
             resources,
             &self.clip_store,
         );
 
-        // Update the state of any picture tile caches. This is a no-op on most
-        // frames (it only does work the first time a new scene is built, or if
-        // the tile-relative transform dependencies have changed).
-        let mut tile_cache_state = TileCacheUpdateState::new();
-        self.prim_store.update_tile_cache(
-            self.root_pic_index,
-            &mut tile_cache_state,
-            &frame_context,
-            resource_cache,
-            resources,
-            &self.clip_store,
-            &pic_update_state.surfaces,
-            gpu_cache,
-            &mut retained_tiles,
-            scratch,
-        );
-
         {
             let visibility_context = FrameVisibilityContext {
                 device_pixel_scale,
                 clip_scroll_tree,
                 screen_world_rect,
                 surfaces: pic_update_state.surfaces,
+                debug_flags,
+                scene_properties,
             };
 
             let mut visibility_state = FrameVisibilityState {
                 resource_cache,
                 gpu_cache,
                 clip_store: &mut self.clip_store,
                 scratch,
+                tile_cache: None,
+                retained_tiles: &mut retained_tiles,
+                resources,
             };
 
             self.prim_store.update_visibility(
                 self.root_pic_index,
                 ROOT_SURFACE_INDEX,
                 &visibility_context,
                 &mut visibility_state,
-                resources,
             );
         }
 
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -1,13 +1,13 @@
 /* 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::{LayoutPrimitiveInfo, LayoutRect};
+use api::{LayoutPrimitiveInfo};
 use internal_types::FastHashMap;
 use malloc_size_of::MallocSizeOf;
 use profiler::ResourceProfileCounter;
 use std::fmt::Debug;
 use std::hash::Hash;
 use std::marker::PhantomData;
 use std::{mem, ops, u64};
 use std::sync::atomic::{AtomicUsize, Ordering};
@@ -422,11 +422,10 @@ pub trait Internable {
     type Source: Eq + Hash + Clone + Debug + MallocSizeOf;
     type StoreData: From<Self::Source> + MallocSizeOf;
     type InternData: MallocSizeOf;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> Self::Source;
 }
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,44 +1,45 @@
 /* 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::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint, WorldPoint};
 use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
-use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize, ClipMode};
+use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize, ClipMode, LayoutSize};
 use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, WorldVector2D, LayoutPoint};
 #[cfg(feature = "debug_renderer")]
 use api::{DebugFlags, DeviceVector2D};
 use box_shadow::{BLUR_SAMPLE_SCALE};
 use clip::{ClipStore, ClipChainId, ClipChainNode, ClipItem};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, CoordinateSystemId};
 #[cfg(feature = "debug_renderer")]
 use debug_colors;
 use device::TextureFilter;
 use euclid::{TypedScale, vec3, TypedRect, TypedPoint2D, TypedSize2D};
 use euclid::approxeq::ApproxEq;
+use frame_builder::FrameVisibilityContext;
 use intern::ItemUid;
 use internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{TransformPalette, TransformPaletteId, UvRectKind};
 use plane_split::{Clipper, Polygon, Splitter};
 use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind};
-use prim_store::{get_raster_rects, CoordinateSpaceMapping, PrimitiveScratchBuffer};
+use prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
 use prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
 use print_tree::PrintTreePrinter;
 use render_backend::FrameResources;
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
 use scene_builder::DocumentResources;
 use smallvec::SmallVec;
-use surface::{SurfaceDescriptor, TransformKey};
+use surface::{SurfaceDescriptor};
 use std::{mem, u16};
 use texture_cache::{Eviction, TextureCacheHandle};
 use tiling::RenderTargetKind;
 use util::{ComparableVec, TransformedRectKind, MatrixHelpers, MaxRect};
 
 /*
  A picture represents a dynamically rendered image. It consists of:
 
@@ -54,25 +55,37 @@ use util::{ComparableVec, TransformedRec
 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.
 pub struct RetainedTiles {
+    /// The tiles retained between display lists.
     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>,
 }
 
 impl RetainedTiles {
     pub fn new() -> Self {
         RetainedTiles {
             tiles: Vec::new(),
+            ref_prims: FastHashMap::default(),
         }
     }
+
+    /// Merge items from one retained tiles into another.
+    pub fn merge(&mut self, other: RetainedTiles) {
+        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>;
@@ -92,25 +105,22 @@ const FRAMES_BEFORE_CACHING: usize = 2;
 //           profiling / telemetry to see when it makes sense
 //           to cache a picture.
 const MAX_CACHE_SIZE: f32 = 2048.0;
 /// The maximum size per axis of a surface,
 ///  in WorldPixel coordinates.
 const MAX_SURFACE_SIZE: f32 = 4096.0;
 
 
-#[derive(Debug)]
-pub struct GlobalTransformInfo {
-    /// Current (quantized) value of the transform, that is
-    /// independent of the value of the spatial node index.
-    /// Only calculated on first use.
-    current: Option<TransformKey>,
-    /// Tiles check this to see if the dependencies have changed.
-    changed: bool,
-}
+/// The number of primitives to search for, trying to correlate
+/// the offset between one display list and another.
+const MAX_PRIMS_TO_CORRELATE: usize = 64;
+/// The minmum number of primitives we need to correlate in
+/// order to consider it a success.
+const MIN_PRIMS_TO_CORRELATE: usize = MAX_PRIMS_TO_CORRELATE / 4;
 
 /// 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,
@@ -128,17 +138,17 @@ impl From<PropertyBinding<f32>> for Opac
         match binding {
             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)]
+#[derive(Debug, Copy, Clone, PartialEq)]
 struct TileId(usize);
 
 /// Information about a cached tile.
 #[derive(Debug)]
 pub struct Tile {
     /// The current world rect of thie tile.
     world_rect: WorldRect,
     /// The current local rect of this tile.
@@ -230,16 +240,18 @@ pub struct PrimitiveDescriptor {
     /// Uniquely identifies the content of the primitive template.
     prim_uid: ItemUid,
     /// The origin in world space of this primitive.
     origin: WorldPoint,
     /// 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
@@ -248,28 +260,28 @@ pub struct TileDescriptor {
 
     /// 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.
-    clip_vertices: ComparableVec<LayoutPoint>,
+    clip_vertices: ComparableVec<PointKey>,
 
     /// List of image keys that this tile depends on.
     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 (quantized) transforms that we care about
+    /// List of the effects of transforms that we care about
     /// tracking for this tile.
-    transforms: ComparableVec<TransformKey>,
+    transforms: ComparableVec<PointKey>,
 }
 
 impl TileDescriptor {
     fn new() -> Self {
         TileDescriptor {
             prims: ComparableVec::new(),
             clip_uids: ComparableVec::new(),
             clip_vertices: ComparableVec::new(),
@@ -317,19 +329,16 @@ pub struct TileCache {
     /// 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>,
-    /// List of transform keys - used to check if transforms
-    /// have changed.
-    transforms: Vec<GlobalTransformInfo>,
     /// List of opacity bindings, with some extra information
     /// about whether they changed since last frame.
     opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
     /// If Some(..) the region that is dirty in this picture.
     pub dirty_region: Option<DirtyRegion>,
     /// If true, we need to update the prim dependencies, due
     /// to relative transforms changing. The dependencies are
     /// stored in each tile, and are a list of things that
@@ -350,39 +359,99 @@ pub struct TileCache {
     /// 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,
     /// Counter for the next id to assign for a new tile.
     next_id: usize,
+    /// List of reference primitive information used for
+    /// correlating the position between display lists.
+    reference_prims: Vec<ReferencePrimitive>,
+    /// The root clip chain for this tile cache.
+    root_clip_chain_id: ClipChainId,
+}
+
+/// Stores information about a primitive in the cache that we will
+/// try to use to correlate positions between display lists.
+struct ReferencePrimitive {
+    uid: ItemUid,
+    local_pos: LayoutPoint,
+    spatial_node_index: SpatialNodeIndex,
+}
+
+/// Collect a sample of primitives from the prim list that can
+/// be used to correlate positions.
+// TODO(gw): Investigate best how to select which primitives to select.
+fn collect_ref_prims(
+    prim_instances: &[PrimitiveInstance],
+    ref_prims: &mut Vec<ReferencePrimitive>,
+    pictures: &[PicturePrimitive],
+) {
+    for prim_instance in prim_instances {
+        if ref_prims.len() >= MAX_PRIMS_TO_CORRELATE {
+            return;
+        }
+
+        match prim_instance.kind {
+            PrimitiveInstanceKind::Picture { pic_index, .. } => {
+                collect_ref_prims(
+                    &pictures[pic_index.0].prim_list.prim_instances,
+                    ref_prims,
+                    pictures,
+                );
+            }
+            _ => {
+                ref_prims.push(ReferencePrimitive {
+                    uid: prim_instance.uid(),
+                    local_pos: prim_instance.prim_origin,
+                    spatial_node_index: prim_instance.spatial_node_index,
+                });
+            }
+        }
+    }
 }
 
 impl TileCache {
-    pub fn new(spatial_node_index: SpatialNodeIndex) -> Self {
+    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 mut reference_prims = Vec::with_capacity(MAX_PRIMS_TO_CORRELATE);
+        collect_ref_prims(
+            prim_instances,
+            &mut reference_prims,
+            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(),
-            transforms: Vec::new(),
             opacity_bindings: FastHashMap::default(),
             dirty_region: None,
             needs_update: true,
             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(),
             next_id: 0,
+            reference_prims,
+            root_clip_chain_id,
         }
     }
 
     fn next_id(&mut self) -> TileId {
         let id = TileId(self.next_id);
         self.next_id += 1;
         id
     }
@@ -408,17 +477,17 @@ impl TileCache {
 
         (p0, p1)
     }
 
     /// Update transforms, opacity bindings and tile rects.
     pub fn pre_update(
         &mut self,
         pic_rect: LayoutRect,
-        frame_context: &FrameBuildingContext,
+        frame_context: &FrameVisibilityContext,
         resource_cache: &ResourceCache,
         retained_tiles: &mut RetainedTiles,
     ) {
         // Work out the scroll offset to apply to the world reference point.
         let scroll_transform = frame_context.clip_scroll_tree.get_relative_transform(
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
         ).expect("bug: unable to get scroll transform");
@@ -428,69 +497,53 @@ impl TileCache {
         );
         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.
-        if !retained_tiles.tiles.is_empty() {
+        let world_offset = if retained_tiles.tiles.is_empty() {
+            None
+        } else {
             assert!(self.tiles.is_empty());
             self.tiles = mem::replace(&mut 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,
+                &mut new_prim_map,
+                frame_context.clip_scroll_tree,
+            );
+
+            // Attempt to correlate them to work out which offset to apply.
+            correlate_prim_maps(
+                &retained_tiles.ref_prims,
+                &new_prim_map,
+            )
+        }.unwrap_or(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,
         );
 
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
-        // Walk the transforms and see if we need to rebuild the primitive
-        // dependencies for each tile.
-        // TODO(gw): We could be smarter here and only rebuild for the primitives
-        //           which are affected by transforms that have changed.
-        if self.transforms.len() == frame_context.clip_scroll_tree.spatial_nodes.len() {
-            for (i, transform) in self.transforms.iter_mut().enumerate() {
-                // If this relative transform was used on the previous frame,
-                // update it and store whether it changed for use during
-                // tile invalidation later.
-                if let Some(ref mut current) = transform.current {
-                    let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
-                        self.spatial_node_index,
-                        SpatialNodeIndex::new(i),
-                        frame_context.clip_scroll_tree,
-                    ).expect("todo: handle invalid mappings");
-
-                    let key = mapping.into();
-                    transform.changed = key != *current;
-                    *current = key;
-                }
-            }
-        } else {
-            // If the size of the transforms array changed, just invalidate all the transforms for now.
-            self.transforms.clear();
-
-            for _ in 0 .. frame_context.clip_scroll_tree.spatial_nodes.len() {
-                self.transforms.push(GlobalTransformInfo {
-                    current: None,
-                    changed: true,
-                });
-            }
-        };
-
         // 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),
@@ -521,17 +574,17 @@ impl TileCache {
 
         // 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
+            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.device_pixel_scale;
@@ -570,17 +623,20 @@ impl TileCache {
         let y_tiles = ((p1.y - p0.y) / TILE_SIZE_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.device_pixel_scale;
-            let key = (tile_device_pos.x.round() as i32, tile_device_pos.y.round() as i32);
+            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.device_pixel_scale.0,
             p0.y / frame_context.device_pixel_scale.0,
         );
@@ -665,72 +721,50 @@ impl TileCache {
             }
         }
     }
 
     /// Update the dependencies for each tile for a given primitive instance.
     pub fn update_prim_dependencies(
         &mut self,
         prim_instance: &PrimitiveInstance,
-        prim_list: &PrimitiveList,
         clip_scroll_tree: &ClipScrollTree,
         resources: &FrameResources,
         clip_chain_nodes: &[ClipChainNode],
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         if !self.needs_update {
             return;
         }
 
-        // We need to ensure that if a primitive belongs to a cluster that has
-        // been marked invisible, we exclude it here. Otherwise, we may end up
-        // with a primitive that is outside the bounding rect of the calculated
-        // picture rect (which takes the cluster visibility into account).
-        if !prim_list.clusters[prim_instance.cluster_index.0 as usize].is_visible {
-            return;
-        }
-
         self.map_local_to_world.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
         let prim_data = &resources.as_common_data(&prim_instance);
 
-        let (prim_rect, clip_rect) = match prim_instance.kind {
+        let prim_rect = match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index, .. } => {
                 let pic = &pictures[pic_index.0];
-                (pic.local_rect, LayoutRect::max_rect())
+                pic.local_rect
             }
             _ => {
-                let prim_rect = LayoutRect::new(
+                LayoutRect::new(
                     prim_instance.prim_origin,
                     prim_data.prim_size,
-                );
-                let clip_rect = prim_data
-                    .prim_relative_clip_rect
-                    .translate(&prim_instance.prim_origin.to_vector());
-
-                (prim_rect, clip_rect)
+                )
             }
         };
 
-        // Map the primitive local rect into the picture space.
-        // TODO(gw): We should maybe store this in the primitive template
-        //           during interning so that we never have to calculate
-        //           it during frame building.
-        let culling_rect = match prim_rect.intersection(&clip_rect) {
-            Some(rect) => rect,
-            None => return,
-        };
-
-        let world_rect = match self.map_local_to_world.map(&culling_rect) {
+        // Map the primitive local rect into world space.
+        let world_rect = match self.map_local_to_world.map(&prim_rect) {
             Some(rect) => rect,
             None => {
                 return;
             }
         };
 
         // If the rect is invalid, no need to create dependencies.
         // TODO(gw): Need to handle pictures with filters here.
@@ -739,28 +773,24 @@ impl TileCache {
         }
 
         // Get the tile coordinates in the picture space.
         let (p0, p1) = self.get_tile_coords_for_rect(&world_rect);
 
         // 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<[LayoutPoint; 8]> = SmallVec::new();
+        let mut clip_vertices: SmallVec<[WorldPoint; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
-        let mut current_clip_chain_id = prim_instance.clip_chain_id;
         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.
-        // TODO(gw): We could also trivially track and exclude the root iframe / content
-        //           clip chain id, since we know that will exist on every item but never
-        //           actually be relevant.
-        let mut world_clips: FastHashMap<RectangleKey, SpatialNodeIndex> = FastHashMap::default();
+        let mut world_clips: SmallVec<[(RectangleKey, SpatialNodeIndex); 4]> = SmallVec::default();
 
         // Some primitives can not be cached (e.g. external video images)
         let is_cacheable = prim_instance.is_cacheable(
             &resources,
             resource_cache,
         );
 
         // For pictures, we don't (yet) know the valid clip rect, so we can't correctly
@@ -824,54 +854,71 @@ impl TileCache {
                 // These don't contribute dependencies
                 true
             }
         };
 
         // 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(LayoutRect::zero());
+
+        let mut current_clip_chain_id = prim_instance.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 = &resources.clip_data_store[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];
 
-                    // 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.
-                    if clip_spatial_node.coordinate_system_id == CoordinateSystemId(0) {
-                        let local_rect = LayoutRect::new(
-                            clip_chain_node.local_pos,
-                            size,
-                        );
+                    let local_clip_rect = LayoutRect::new(
+                        clip_chain_node.local_pos,
+                        size,
+                    );
 
-                        match self.map_local_to_world.map(&local_rect) {
+                    // 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(LayoutRect::zero());
+                        false
+                    } else 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(WorldRect::zero());
 
-                                world_clips.insert(
+                                world_clips.push((
                                     clip_world_rect.into(),
                                     clip_chain_node.spatial_node_index,
-                                );
+                                ));
 
                                 false
                             }
                             None => {
                                 true
                             }
                         }
                     } else {
@@ -881,43 +928,68 @@ impl TileCache {
                 ClipItem::Rectangle(_, ClipMode::ClipOut) |
                 ClipItem::RoundedRectangle(..) |
                 ClipItem::Image { .. } |
                 ClipItem::BoxShadow(..) => {
                     true
                 }
             };
 
-            clip_vertices.push(clip_chain_node.local_pos);
-            clip_chain_uids.push(clip_chain_node.handle.uid());
+            if add_to_clip_deps {
+                clip_chain_uids.push(clip_chain_node.handle.uid());
+                clip_spatial_nodes.insert(clip_chain_node.spatial_node_index);
 
-            if add_to_clip_deps {
-                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 include_clip_rect {
             self.world_bounding_rect = self.world_bounding_rect.union(&world_clip_rect);
         }
 
+        self.map_local_to_world.set_target_spatial_node(
+            prim_instance.spatial_node_index,
+            clip_scroll_tree,
+        );
+        let world_culling_rect = self
+            .map_local_to_world
+            .map(&culling_rect)
+            .expect("bug: unable to map local clip rect");
+
         // 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 {
                 // If the primitive exists on tiles outside the selected tile cache
                 // area, just ignore those.
                 if x < 0 || x >= self.tile_count.width || y < 0 || y >= self.tile_count.height {
                     continue;
                 }
 
                 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(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).
 
                 // Mark if the tile is cacheable at all.
                 tile.is_same_content &= is_cacheable;
 
@@ -925,22 +997,26 @@ impl TileCache {
                 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);
 
                 // 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(),
+                    origin: (world_rect.origin - tile.world_rect.origin.to_vector()).round(),
                     first_clip: tile.descriptor.clip_uids.len() as u16,
                     clip_count: clip_chain_uids.len() as u16,
+                    world_culling_rect,
                 });
                 tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
-                tile.descriptor.clip_vertices.extend_from_slice(&clip_vertices);
+                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.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);
                 }
@@ -950,17 +1026,17 @@ impl TileCache {
 
     /// 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: &FrameBuildingContext,
+        frame_context: &FrameVisibilityContext,
         _scratch: &mut PrimitiveScratchBuffer,
     ) -> LayoutRect {
         let mut dirty_world_rect = WorldRect::zero();
 
         self.dirty_region = None;
         self.pending_blits.clear();
 
         let descriptor = ImageDescriptor::new(
@@ -999,22 +1075,26 @@ impl TileCache {
                     tile.transforms.insert(*spatial_node_index);
                 }
             }
 
             // 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 {
-                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
+                let xf = frame_context.clip_scroll_tree.get_relative_transform(
                     self.spatial_node_index,
                     spatial_node_index,
-                    frame_context.clip_scroll_tree,
-                ).expect("todo: handle invalid mappings");
-                tile.descriptor.transforms.push(mapping.into());
+                ).expect("BUG: unable to get relative transform");
+                // 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 = xf.transform_point2d(&LayoutPoint::zero()).unwrap_or(LayoutPoint::zero()).round();
+                tile.descriptor.transforms.push(key.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
@@ -1141,29 +1221,16 @@ impl TileCache {
                 dirty_device_rect: dirty_device_rect.round().to_i32(),
             })
         };
 
         local_clip_rect
     }
 }
 
-/// State structure that is used during the tile cache update picture traversal.
-pub struct TileCacheUpdateState {
-    pub tile_cache: Option<TileCache>,
-}
-
-impl TileCacheUpdateState {
-    pub fn new() -> Self {
-        TileCacheUpdateState {
-            tile_cache: None,
-        }
-    }
-}
-
 /// Maintains a stack of picture and surface information, that
 /// is used during the initial picture traversal.
 pub struct PictureUpdateState<'a> {
     pub surfaces: &'a mut Vec<SurfaceInfo>,
     surface_stack: Vec<SurfaceIndex>,
     picture_stack: Vec<PictureInfo>,
 }
 
@@ -1540,20 +1607,17 @@ impl PrimitiveList {
             // 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,
                 );
-                let clip_rect = prim_data
-                    .prim_relative_clip_rect
-                    .translate(&prim_instance.prim_origin.to_vector());
-                let culling_rect = clip_rect
+                let culling_rect = prim_instance.local_clip_rect
                     .intersection(&prim_rect)
                     .unwrap_or(LayoutRect::zero());
 
                 cluster.bounding_rect = cluster.bounding_rect.union(&culling_rect);
             }
 
             prim_instance.cluster_index = ClusterIndex(cluster_index as u16);
         }
@@ -1676,18 +1740,27 @@ 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,
+                &mut retained_tiles.ref_prims,
+                clip_scroll_tree,
+            );
+
             for tile in tile_cache.tiles {
                 retained_tiles.tiles.push(tile);
             }
         }
     }
 
     pub fn new_image(
         requested_composite_mode: Option<PictureCompositeMode>,
@@ -1695,16 +1768,17 @@ impl PicturePrimitive {
         pipeline_id: PipelineId,
         frame_output_pipeline_id: Option<PipelineId>,
         apply_local_clip_rect: bool,
         requested_raster_space: RasterSpace,
         prim_list: PrimitiveList,
         spatial_node_index: SpatialNodeIndex,
         local_clip_rect: LayoutRect,
         clip_store: &ClipStore,
+        tile_cache: Option<TileCache>,
     ) -> Self {
         // For now, only create a cache descriptor for blur filters (which
         // includes text shadows). We can incrementally expand this to
         // handle more composite modes.
         let create_cache_descriptor = match requested_composite_mode {
             Some(PictureCompositeMode::Filter(FilterOp::Blur(blur_radius))) => {
                 blur_radius > 0.0
             }
@@ -1718,25 +1792,16 @@ impl PicturePrimitive {
                 &prim_list.prim_instances,
                 spatial_node_index,
                 clip_store,
             )
         } else {
             None
         };
 
-        let tile_cache = match requested_composite_mode {
-            Some(PictureCompositeMode::TileCache { .. }) => {
-                Some(TileCache::new(spatial_node_index))
-            }
-            Some(_) | None => {
-                None
-            }
-        };
-
         PicturePrimitive {
             surface_desc,
             prim_list,
             state: None,
             secondary_render_task_id: None,
             requested_composite_mode,
             raster_config: None,
             context_3d,
@@ -2106,44 +2171,16 @@ impl PicturePrimitive {
                 establishes_raster_root: surface_spatial_node_index == surface.raster_spatial_node_index,
                 surface_index: state.push_surface(surface),
             });
         }
 
         Some(mem::replace(&mut self.prim_list.pictures, SmallVec::new()))
     }
 
-    /// Update the primitive dependencies for any active tile caches,
-    /// but only *if* the transforms have made the mappings out of date.
-    pub fn update_prim_dependencies(
-        &self,
-        tile_cache: &mut TileCache,
-        frame_context: &FrameBuildingContext,
-        resource_cache: &mut ResourceCache,
-        resources: &FrameResources,
-        pictures: &[PicturePrimitive],
-        clip_store: &ClipStore,
-        opacity_binding_store: &OpacityBindingStorage,
-        image_instances: &ImageInstanceStorage,
-    ) {
-        for prim_instance in &self.prim_list.prim_instances {
-            tile_cache.update_prim_dependencies(
-                prim_instance,
-                &self.prim_list,
-                &frame_context.clip_scroll_tree,
-                resources,
-                &clip_store.clip_chain_nodes,
-                pictures,
-                resource_cache,
-                opacity_binding_store,
-                image_instances,
-            );
-        }
-    }
-
     /// Called after updating child pictures during the initial
     /// picture traversal.
     pub fn post_update(
         &mut self,
         child_pictures: PictureList,
         state: &mut PictureUpdateState,
         frame_context: &FrameBuildingContext,
         gpu_cache: &mut GpuCache,
@@ -2769,8 +2806,87 @@ 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:
+            // (a) We found more than one quarter of the selected
+            //     reference primitives to have the same offset.
+            // (b) The display lists both had the same number of
+            //     primitives, and we exactly matched. This handles
+            //     edge cases like scenes where there are very
+            //     few primitives, while excluding edge cases like
+            //     dl_mutate that have thousands of primitives with
+            //     the same uid.
+            if (count >= MIN_PRIMS_TO_CORRELATE) ||
+               (count == old_prims.len() && count == new_prims.len()) {
+                Some(offset.into())
+            } else {
+                None
+            }
+        })
+}
--- a/gfx/wr/webrender/src/prim_store/borders.rs
+++ b/gfx/wr/webrender/src/prim_store/borders.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{
-    AuHelpers, LayoutPrimitiveInfo, LayoutRect, LayoutSideOffsets,
+    AuHelpers, LayoutPrimitiveInfo, LayoutSideOffsets,
     LayoutSideOffsetsAu, LayoutSize, NormalBorder, PremultipliedColorF,
     Shadow
 };
 use border::create_border_segments;
 use border::NormalBorderAu;
 use display_list_flattener::{AsInstanceKind, CreateShadow, IsVisible};
 use frame_builder::{FrameBuildingState};
 use gpu_cache::GpuDataRequest;
@@ -30,23 +30,21 @@ pub struct NormalBorderPrim {
     pub widths: LayoutSideOffsetsAu,
 }
 
 pub type NormalBorderKey = PrimKey<NormalBorderPrim>;
 
 impl NormalBorderKey {
     pub fn new(
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
         normal_border: NormalBorderPrim,
     ) -> Self {
         NormalBorderKey {
             common: PrimKeyCommonData::with_info(
                 info,
-                prim_relative_clip_rect,
             ),
             kind: normal_border,
         }
     }
 }
 
 impl intern::InternDebug for NormalBorderKey {}
 
@@ -175,21 +173,19 @@ impl intern::Internable for NormalBorder
     type Source = NormalBorderKey;
     type StoreData = NormalBorderTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> NormalBorderKey {
         NormalBorderKey::new(
             info,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 impl CreateShadow for NormalBorderPrim {
     fn create_shadow(&self, shadow: &Shadow) -> Self {
         let border = self.border.with_color(shadow.color.into());
@@ -217,23 +213,21 @@ pub struct ImageBorder {
     pub nine_patch: NinePatchDescriptor,
 }
 
 pub type ImageBorderKey = PrimKey<ImageBorder>;
 
 impl ImageBorderKey {
     pub fn new(
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
         image_border: ImageBorder,
     ) -> Self {
         ImageBorderKey {
             common: PrimKeyCommonData::with_info(
                 info,
-                prim_relative_clip_rect,
             ),
             kind: image_border,
         }
     }
 }
 
 impl intern::InternDebug for ImageBorderKey {}
 
@@ -356,21 +350,19 @@ impl intern::Internable for ImageBorder 
     type Source = ImageBorderKey;
     type StoreData = ImageBorderTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> ImageBorderKey {
         ImageBorderKey::new(
             info,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 impl IsVisible for ImageBorder {
     fn is_visible(&self) -> bool {
         true
@@ -383,14 +375,14 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<NormalBorderPrim>(), 84, "NormalBorderPrim size changed");
-    assert_eq!(mem::size_of::<NormalBorderTemplate>(), 224, "NormalBorderTemplate size changed");
-    assert_eq!(mem::size_of::<NormalBorderKey>(), 112, "NormalBorderKey size changed");
+    assert_eq!(mem::size_of::<NormalBorderTemplate>(), 208, "NormalBorderTemplate size changed");
+    assert_eq!(mem::size_of::<NormalBorderKey>(), 96, "NormalBorderKey size changed");
     assert_eq!(mem::size_of::<ImageBorder>(), 92, "ImageBorder size changed");
-    assert_eq!(mem::size_of::<ImageBorderTemplate>(), 88, "ImageBorderTemplate size changed");
-    assert_eq!(mem::size_of::<ImageBorderKey>(), 120, "ImageBorderKey size changed");
+    assert_eq!(mem::size_of::<ImageBorderTemplate>(), 72, "ImageBorderTemplate size changed");
+    assert_eq!(mem::size_of::<ImageBorderKey>(), 104, "ImageBorderKey size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/gradient.rs
+++ b/gfx/wr/webrender/src/prim_store/gradient.rs
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{
     ColorF, ColorU,ExtendMode, GradientStop, LayoutPoint, LayoutSize,
-    LayoutPrimitiveInfo, LayoutRect, PremultipliedColorF
+    LayoutPrimitiveInfo, PremultipliedColorF
 };
 use display_list_flattener::{AsInstanceKind, IsVisible};
 use frame_builder::FrameBuildingState;
 use gpu_cache::{GpuCacheHandle, GpuDataRequest};
 use intern::{DataStore, Handle, Internable, InternDebug, Interner, UpdateList};
 use prim_store::{BrushSegment, GradientTileRange};
 use prim_store::{PrimitiveInstanceKind, PrimitiveOpacity, PrimitiveSceneData};
 use prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
@@ -50,24 +50,22 @@ pub struct LinearGradientKey {
     pub reverse_stops: bool,
     pub nine_patch: Option<Box<NinePatchDescriptor>>,
 }
 
 impl LinearGradientKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         linear_grad: LinearGradient,
     ) -> Self {
         LinearGradientKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             extend_mode: linear_grad.extend_mode,
             start_point: linear_grad.start_point,
             end_point: linear_grad.end_point,
             stretch_size: linear_grad.stretch_size,
             tile_spacing: linear_grad.tile_spacing,
             stops: linear_grad.stops,
             reverse_stops: linear_grad.reverse_stops,
@@ -253,22 +251,20 @@ impl Internable for LinearGradient {
     type Source = LinearGradientKey;
     type StoreData = LinearGradientTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect
     ) -> LinearGradientKey {
         LinearGradientKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self
         )
     }
 }
 
 impl IsVisible for LinearGradient {
     fn is_visible(&self) -> bool {
         true
@@ -311,24 +307,22 @@ pub struct RadialGradientKey {
     pub tile_spacing: SizeKey,
     pub nine_patch: Option<Box<NinePatchDescriptor>>,
 }
 
 impl RadialGradientKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         radial_grad: RadialGradient,
     ) -> Self {
         RadialGradientKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             extend_mode: radial_grad.extend_mode,
             center: radial_grad.center,
             params: radial_grad.params,
             stretch_size: radial_grad.stretch_size,
             stops: radial_grad.stops,
             tile_spacing: radial_grad.tile_spacing,
             nine_patch: radial_grad.nine_patch,
@@ -483,22 +477,20 @@ impl Internable for RadialGradient {
     type Source = RadialGradientKey;
     type StoreData = RadialGradientTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> RadialGradientKey {
         RadialGradientKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 impl IsVisible for RadialGradient {
     fn is_visible(&self) -> bool {
         true
@@ -721,15 +713,15 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed");
-    assert_eq!(mem::size_of::<LinearGradientTemplate>(), 128, "LinearGradientTemplate size changed");
-    assert_eq!(mem::size_of::<LinearGradientKey>(), 96, "LinearGradientKey size changed");
+    assert_eq!(mem::size_of::<LinearGradientTemplate>(), 112, "LinearGradientTemplate size changed");
+    assert_eq!(mem::size_of::<LinearGradientKey>(), 80, "LinearGradientKey size changed");
 
     assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed");
-    assert_eq!(mem::size_of::<RadialGradientTemplate>(), 136, "RadialGradientTemplate size changed");
-    assert_eq!(mem::size_of::<RadialGradientKey>(), 104, "RadialGradientKey size changed");
+    assert_eq!(mem::size_of::<RadialGradientTemplate>(), 120, "RadialGradientTemplate size changed");
+    assert_eq!(mem::size_of::<RadialGradientKey>(), 88, "RadialGradientKey size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/image.rs
+++ b/gfx/wr/webrender/src/prim_store/image.rs
@@ -79,25 +79,23 @@ pub struct Image {
 }
 
 pub type ImageKey = PrimKey<Image>;
 
 impl ImageKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         image: Image,
     ) -> Self {
 
         ImageKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             kind: image,
         }
     }
 }
 
 impl InternDebug for ImageKey {}
 
@@ -350,22 +348,20 @@ impl Internable for Image {
     type Source = ImageKey;
     type StoreData = ImageTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> ImageKey {
         ImageKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self
         )
     }
 }
 
 impl CreateShadow for Image {
     fn create_shadow(&self, shadow: &Shadow) -> Self {
         Image {
@@ -400,25 +396,23 @@ pub struct YuvImage {
 }
 
 pub type YuvImageKey = PrimKey<YuvImage>;
 
 impl YuvImageKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         yuv_image: YuvImage,
     ) -> Self {
 
         YuvImageKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             kind: yuv_image,
         }
     }
 }
 
 impl InternDebug for YuvImageKey {}
 
@@ -528,22 +522,20 @@ impl Internable for YuvImage {
     type Source = YuvImageKey;
     type StoreData = YuvImageTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> YuvImageKey {
         YuvImageKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self
         )
     }
 }
 
 impl IsVisible for YuvImage {
     fn is_visible(&self) -> bool {
         true
@@ -556,14 +548,14 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<Image>(), 56, "Image size changed");
-    assert_eq!(mem::size_of::<ImageTemplate>(), 124, "ImageTemplate size changed");
-    assert_eq!(mem::size_of::<ImageKey>(), 84, "ImageKey size changed");
+    assert_eq!(mem::size_of::<ImageTemplate>(), 108, "ImageTemplate size changed");
+    assert_eq!(mem::size_of::<ImageKey>(), 68, "ImageKey size changed");
     assert_eq!(mem::size_of::<YuvImage>(), 36, "YuvImage size changed");
-    assert_eq!(mem::size_of::<YuvImageTemplate>(), 72, "YuvImageTemplate size changed");
-    assert_eq!(mem::size_of::<YuvImageKey>(), 64, "YuvImageKey size changed");
+    assert_eq!(mem::size_of::<YuvImageTemplate>(), 56, "YuvImageTemplate size changed");
+    assert_eq!(mem::size_of::<YuvImageKey>(), 48, "YuvImageKey size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/line_dec.rs
+++ b/gfx/wr/webrender/src/prim_store/line_dec.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{
-    ColorF, ColorU, LayoutPrimitiveInfo, LayoutRect, LayoutSizeAu,
+    ColorF, ColorU, LayoutPrimitiveInfo, LayoutSizeAu,
     LineOrientation, LineStyle, PremultipliedColorF, Shadow,
 };
 use app_units::Au;
 use display_list_flattener::{AsInstanceKind, CreateShadow, IsVisible};
 use frame_builder::{FrameBuildingState};
 use gpu_cache::GpuDataRequest;
 use intern;
 use prim_store::{
@@ -40,23 +40,21 @@ pub struct LineDecoration {
     pub color: ColorU,
 }
 
 pub type LineDecorationKey = PrimKey<LineDecoration>;
 
 impl LineDecorationKey {
     pub fn new(
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
         line_dec: LineDecoration,
     ) -> Self {
         LineDecorationKey {
             common: PrimKeyCommonData::with_info(
                 info,
-                prim_relative_clip_rect,
             ),
             kind: line_dec,
         }
     }
 }
 
 impl intern::InternDebug for LineDecorationKey {}
 
@@ -150,21 +148,19 @@ impl intern::Internable for LineDecorati
     type Source = LineDecorationKey;
     type StoreData = LineDecorationTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> LineDecorationKey {
         LineDecorationKey::new(
             info,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 impl CreateShadow for LineDecoration {
     fn create_shadow(&self, shadow: &Shadow) -> Self {
         LineDecoration {
@@ -186,11 +182,11 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<LineDecoration>(), 20, "LineDecoration size changed");
-    assert_eq!(mem::size_of::<LineDecorationTemplate>(), 68, "LineDecorationTemplate size changed");
-    assert_eq!(mem::size_of::<LineDecorationKey>(), 48, "LineDecorationKey size changed");
+    assert_eq!(mem::size_of::<LineDecorationTemplate>(), 52, "LineDecorationTemplate size changed");
+    assert_eq!(mem::size_of::<LineDecorationKey>(), 32, "LineDecorationKey size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{BorderRadius, ClipMode, ColorF, PictureRect, ColorU, LayoutVector2D};
-use api::{DeviceIntRect, DevicePixelScale, DeviceRect};
+use api::{DeviceIntRect, DevicePixelScale, DeviceRect, WorldVector2D};
 use api::{FilterOp, ImageRendering, TileOffset, RepeatMode, WorldPoint, WorldSize};
 use api::{LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize};
 use api::{PremultipliedColorF, PropertyBinding, Shadow};
 use api::{WorldPixel, BoxShadowClipMode, WorldRect, LayoutToWorldScale};
 use api::{PicturePixel, RasterPixel, LineStyle, LineOrientation, AuHelpers};
 use api::{LayoutPrimitiveInfo};
 #[cfg(feature = "debug_renderer")]
 use api::DevicePoint;
@@ -24,38 +24,38 @@ use euclid::{SideOffsets2D, TypedTransfo
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::{PrimitiveContext, FrameVisibilityContext, FrameVisibilityState};
 use glyph_rasterizer::GlyphKey;
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks};
 use gpu_types::BrushFlags;
 use image::{Repetition};
 use intern;
 use malloc_size_of::MallocSizeOf;
-use picture::{PictureCompositeMode, PicturePrimitive, PictureUpdateState, TileCacheUpdateState};
-use picture::{ClusterIndex, PrimitiveList, SurfaceIndex, SurfaceInfo, RetainedTiles, RasterConfig};
+use picture::{PictureCompositeMode, PicturePrimitive, PictureUpdateState};
+use picture::{ClusterIndex, PrimitiveList, SurfaceIndex, RetainedTiles, RasterConfig};
 use prim_store::borders::{ImageBorderDataHandle, NormalBorderDataHandle};
 use prim_store::gradient::{LinearGradientDataHandle, RadialGradientDataHandle};
 use prim_store::image::{ImageDataHandle, ImageInstance, VisibleImageTile, YuvImageDataHandle};
 use prim_store::line_dec::LineDecorationDataHandle;
 use prim_store::picture::PictureDataHandle;
 use prim_store::text_run::{TextRunDataHandle, TextRunPrimitive};
 #[cfg(debug_assertions)]
 use render_backend::{FrameId};
 use render_backend::FrameResources;
 use render_task::{RenderTask, RenderTaskCacheKey, to_cache_size};
 use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
 use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
-use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
+use resource_cache::{ImageProperties, ImageRequest};
 use scene::SceneProperties;
 use segment::SegmentBuilder;
 use std::{cmp, fmt, hash, ops, u32, usize, mem};
 #[cfg(debug_assertions)]
 use std::sync::atomic::{AtomicUsize, Ordering};
 use storage;
-use util::{ScaleOffset, MatrixHelpers, MaxRect, recycle_vec};
+use util::{ScaleOffset, MatrixHelpers, recycle_vec, MaxRect};
 use util::{pack_as_float, project_rect, raster_rect_to_device_pixels};
 use smallvec::SmallVec;
 
 pub mod borders;
 pub mod gradient;
 pub mod image;
 pub mod line_dec;
 pub mod picture;
@@ -346,17 +346,16 @@ impl GpuCacheAddress {
 /// The information about an interned primitive that
 /// is stored and available in the scene builder
 /// thread.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(MallocSizeOf)]
 pub struct PrimitiveSceneData {
     pub prim_size: LayoutSize,
-    pub prim_relative_clip_rect: LayoutRect,
     pub is_backface_visible: bool,
 }
 
 /// Information specific to a primitive type that
 /// uniquely identifies a primitive template by key.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
@@ -536,25 +535,40 @@ impl hash::Hash for VectorKey {
 }
 
 impl From<VectorKey> for LayoutVector2D {
     fn from(key: VectorKey) -> LayoutVector2D {
         LayoutVector2D::new(key.x, key.y)
     }
 }
 
+impl From<VectorKey> for WorldVector2D {
+    fn from(key: VectorKey) -> WorldVector2D {
+        WorldVector2D::new(key.x, key.y)
+    }
+}
+
 impl From<LayoutVector2D> for VectorKey {
     fn from(vec: LayoutVector2D) -> VectorKey {
         VectorKey {
             x: vec.x,
             y: vec.y,
         }
     }
 }
 
+impl From<WorldVector2D> for VectorKey {
+    fn from(vec: WorldVector2D) -> VectorKey {
+        VectorKey {
+            x: vec.x,
+            y: vec.y,
+        }
+    }
+}
+
 /// A hashable point for using as a key during primitive interning.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, MallocSizeOf, PartialEq)]
 pub struct PointKey {
     pub x: f32,
     pub y: f32,
 }
@@ -578,34 +592,40 @@ impl From<LayoutPoint> for PointKey {
     fn from(p: LayoutPoint) -> 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,
+        }
+    }
+}
+
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
 pub struct PrimKeyCommonData {
     pub is_backface_visible: bool,
     pub prim_size: SizeKey,
-    pub prim_relative_clip_rect: RectangleKey,
 }
 
 impl PrimKeyCommonData {
     pub fn with_info(
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> Self {
         PrimKeyCommonData {
             is_backface_visible: info.is_backface_visible,
             prim_size: info.rect.size.into(),
-            prim_relative_clip_rect: prim_relative_clip_rect.into(),
         }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
 pub struct PrimKey<T: MallocSizeOf> {
@@ -620,24 +640,22 @@ pub struct PrimitiveKey {
     pub common: PrimKeyCommonData,
     pub kind: PrimitiveKeyKind,
 }
 
 impl PrimitiveKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         kind: PrimitiveKeyKind,
     ) -> Self {
         PrimitiveKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             kind,
         }
     }
 }
 
 impl intern::InternDebug for PrimitiveKey {}
 
@@ -697,31 +715,29 @@ impl PrimitiveKeyKind {
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(MallocSizeOf)]
 pub struct PrimTemplateCommonData {
     pub is_backface_visible: bool,
     pub prim_size: LayoutSize,
-    pub prim_relative_clip_rect: LayoutRect,
     pub opacity: PrimitiveOpacity,
     /// The GPU cache handle for a primitive template. Since this structure
     /// is retained across display lists by interning, this GPU cache handle
     /// also remains valid, which reduces the number of updates to the GPU
     /// cache when a new display list is processed.
     pub gpu_cache_handle: GpuCacheHandle,
 }
 
 impl PrimTemplateCommonData {
     pub fn with_key_common(common: PrimKeyCommonData) -> Self {
         PrimTemplateCommonData {
             is_backface_visible: common.is_backface_visible,
             prim_size: common.prim_size.into(),
-            prim_relative_clip_rect: common.prim_relative_clip_rect.into(),
             gpu_cache_handle: GpuCacheHandle::new(),
             opacity: PrimitiveOpacity::translucent(),
         }
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -813,22 +829,20 @@ impl intern::Internable for PrimitiveKey
     type Marker = PrimitiveDataMarker;
     type Source = PrimitiveKey;
     type StoreData = PrimitiveTemplate;
     type InternData = PrimitiveSceneData;
 
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> PrimitiveKey {
         PrimitiveKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 pub type PrimitiveDataStore = intern::DataStore<PrimitiveKey, PrimitiveTemplate, PrimitiveDataMarker>;
 pub type PrimitiveDataHandle = intern::Handle<PrimitiveDataMarker>;
 pub type PrimitiveDataUpdateList = intern::UpdateList<PrimitiveKey>;
@@ -1388,16 +1402,19 @@ pub struct PrimitiveInstance {
     /// the relevant information for the primitive
     /// can be found.
     pub kind: PrimitiveInstanceKind,
 
     /// Local space origin of this primitive. The size
     /// of the primitive is defined by the template.
     pub prim_origin: LayoutPoint,
 
+    /// Local space clip rect for this instance
+    pub local_clip_rect: LayoutRect,
+
     #[cfg(debug_assertions)]
     pub id: PrimitiveDebugId,
 
     /// The last frame ID (of the `RenderTaskTree`) this primitive
     /// was prepared for rendering in.
     #[cfg(debug_assertions)]
     pub prepared_frame_id: FrameId,
 
@@ -1415,22 +1432,24 @@ pub struct PrimitiveInstance {
 
     /// ID of the spatial node that this primitive is positioned by.
     pub spatial_node_index: SpatialNodeIndex,
 }
 
 impl PrimitiveInstance {
     pub fn new(
         prim_origin: LayoutPoint,
+        local_clip_rect: LayoutRect,
         kind: PrimitiveInstanceKind,
         clip_chain_id: ClipChainId,
         spatial_node_index: SpatialNodeIndex,
     ) -> Self {
         PrimitiveInstance {
             prim_origin,
+            local_clip_rect,
             kind,
             #[cfg(debug_assertions)]
             prepared_frame_id: FrameId::INVALID,
             #[cfg(debug_assertions)]
             id: PrimitiveDebugId(NEXT_PRIM_ID.fetch_add(1, Ordering::Relaxed)),
             visibility_info: PrimitiveVisibilityIndex::INVALID,
             clip_chain_id,
             spatial_node_index,
@@ -1684,20 +1703,22 @@ 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(
         self,
         retained_tiles: &mut RetainedTiles,
+        clip_scroll_tree: &ClipScrollTree,
     ) {
         for pic in 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()
@@ -1744,27 +1765,42 @@ 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,
         frame_context: &FrameVisibilityContext,
         frame_state: &mut FrameVisibilityState,
-        resources: &mut FrameResources,
     ) {
         let (mut prim_list, surface_index, apply_local_clip_rect) = {
             let pic = &mut self.pictures[pic_index.0];
 
             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,
             };
 
+            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.local_rect,
+                    frame_context,
+                    frame_state.resource_cache,
+                    frame_state.retained_tiles,
+                );
+
+                frame_state.tile_cache = Some(tile_cache);
+            }
+
             (prim_list, surface_index, pic.apply_local_clip_rect)
         };
 
         let surface = &frame_context.surfaces[surface_index.0 as usize];
 
         let mut map_local_to_surface = surface
             .map_local_to_surface
             .clone();
@@ -1802,17 +1838,17 @@ impl PrimitiveStore {
                 prim_instance.spatial_node_index,
             );
 
             map_local_to_surface.set_target_spatial_node(
                 prim_instance.spatial_node_index,
                 frame_context.clip_scroll_tree,
             );
 
-            let (is_passthrough, prim_local_rect, prim_local_clip_rect, clip_node_collector) = match prim_instance.kind {
+            let (is_passthrough, prim_local_rect, clip_node_collector) = match prim_instance.kind {
                 PrimitiveInstanceKind::Picture { pic_index, .. } => {
                     if !self.pictures[pic_index.0].is_visible() {
                         continue;
                     }
 
                     if let Some(ref raster_config) = self.pictures[pic_index.0].raster_config {
                         if raster_config.establishes_raster_root {
                             let surface = &frame_context.surfaces[raster_config.surface_index.0 as usize];
@@ -1820,52 +1856,48 @@ impl PrimitiveStore {
                         }
                     }
 
                     self.update_visibility(
                         pic_index,
                         surface_index,
                         frame_context,
                         frame_state,
-                        resources,
                     );
 
                     let pic = &self.pictures[pic_index.0];
 
                     let clip_node_collector = pic.raster_config.as_ref().and_then(|rc| {
                         if rc.establishes_raster_root {
                             Some(frame_state.clip_store.pop_raster_root())
                         } else {
                             None
                         }
                     });
 
-                    (pic.raster_config.is_none(), pic.local_rect, LayoutRect::max_rect(), clip_node_collector)
+                    (pic.raster_config.is_none(), pic.local_rect, clip_node_collector)
                 }
                 _ => {
-                    let prim_data = &resources.as_common_data(&prim_instance);
+                    let prim_data = &frame_state.resources.as_common_data(&prim_instance);
 
                     let prim_rect = LayoutRect::new(
                         prim_instance.prim_origin,
                         prim_data.prim_size,
                     );
-                    let clip_rect = prim_data
-                        .prim_relative_clip_rect
-                        .translate(&LayoutVector2D::new(prim_instance.prim_origin.x, prim_instance.prim_origin.y));
-
-                    (false, prim_rect, clip_rect, None)
+
+                    (false, prim_rect, None)
                 }
             };
 
             if is_passthrough {
                 let vis_index = PrimitiveVisibilityIndex(frame_state.scratch.prim_info.len() as u32);
 
                 frame_state.scratch.prim_info.push(
                     PrimitiveVisibility {
-                        clipped_world_rect: WorldRect::zero(),
+                        clipped_world_rect: WorldRect::max_rect(),
                         clip_chain: ClipChainInstance::empty(),
                         clip_task_index: ClipTaskIndex::INVALID,
                         combined_local_clip_rect: LayoutRect::zero(),
                     }
                 );
 
                 prim_instance.visibility_info = vis_index;
             } else {
@@ -1878,46 +1910,59 @@ impl PrimitiveStore {
 
                 // Inflate the local rect for this primitive by the inflation factor of
                 // the picture context. This ensures that even if the primitive itself
                 // is not visible, any effects from the blur radius will be correctly
                 // taken into account.
                 let inflation_factor = surface.inflation_factor;
                 let local_rect = prim_local_rect
                     .inflate(inflation_factor, inflation_factor)
-                    .intersection(&prim_local_clip_rect);
+                    .intersection(&prim_instance.local_clip_rect);
                 let local_rect = match local_rect {
                     Some(local_rect) => local_rect,
                     None => {
                         if prim_instance.is_chased() {
                             println!("\tculled for being out of the local clip rectangle: {:?}",
-                                prim_local_clip_rect);
+                                prim_instance.local_clip_rect);
                         }
                         continue;
                     }
                 };
 
                 let clip_chain = frame_state
                     .clip_store
                     .build_clip_chain_instance(
                         prim_instance,
                         local_rect,
-                        prim_local_clip_rect,
+                        prim_instance.local_clip_rect,
                         prim_context.spatial_node_index,
                         &map_local_to_surface,
                         &map_surface_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_context.device_pixel_scale,
                         &frame_context.screen_world_rect,
                         clip_node_collector.as_ref(),
-                        &mut resources.clip_data_store,
+                        &mut frame_state.resources.clip_data_store,
                     );
 
+                if let Some(ref mut tile_cache) = frame_state.tile_cache {
+                    tile_cache.update_prim_dependencies(
+                        prim_instance,
+                        frame_context.clip_scroll_tree,
+                        frame_state.resources,
+                        &frame_state.clip_store.clip_chain_nodes,
+                        &self.pictures,
+                        frame_state.resource_cache,
+                        &self.opacity_bindings,
+                        &self.images,
+                    );
+                }
+
                 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");
                         }
                         prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                         continue;
@@ -1929,17 +1974,17 @@ impl PrimitiveStore {
                         clip_chain.clips_range,
                         if apply_local_clip_rect { "(applied)" } else { "" },
                     );
                 }
 
                 let combined_local_clip_rect = if apply_local_clip_rect {
                     clip_chain.local_clip_rect
                 } else {
-                    prim_local_clip_rect
+                    prim_instance.local_clip_rect
                 };
 
                 // Check if the clip bounding rect (in pic space) is visible on screen
                 // 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;
@@ -1964,106 +2009,33 @@ impl PrimitiveStore {
                     }
                 );
 
                 prim_instance.visibility_info = vis_index;
             }
 
         }
 
-        // frame_state.pop_picture();
-        self.pictures[pic_index.0].prim_list = prim_list;
-    }
-
-    /// Update any picture tile caches for a subset of the picture tree.
-    /// This is often a no-op that exits very quickly, unless a new scene
-    /// has arrived, or the relative transforms have changed.
-    pub fn update_tile_cache(
-        &mut self,
-        pic_index: PictureIndex,
-        state: &mut TileCacheUpdateState,
-        frame_context: &FrameBuildingContext,
-        resource_cache: &mut ResourceCache,
-        resources: &FrameResources,
-        clip_store: &ClipStore,
-        surfaces: &[SurfaceInfo],
-        gpu_cache: &mut GpuCache,
-        retained_tiles: &mut RetainedTiles,
-        scratch: &mut PrimitiveScratchBuffer,
-    ) {
-        let children = {
-            let pic = &mut self.pictures[pic_index.0];
-            // Only update the tile cache if we ended up selecting tile caching for the
-            // composite mode of this picture. In some cases, even if the requested
-            // composite mode was tile caching, WR may choose not to draw this picture
-            // with tile cache enabled. For now, this is only in the case of very large
-            // picture rects, but in future we may do it for performance reasons too.
-            if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) = pic.raster_config {
-                debug_assert!(state.tile_cache.is_none());
-                let mut tile_cache = pic.tile_cache.take().unwrap();
-
-                // 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.local_rect,
-                    frame_context,
-                    resource_cache,
-                    retained_tiles,
-                );
-
-                state.tile_cache = Some(tile_cache);
-            }
-            mem::replace(&mut pic.prim_list.pictures, SmallVec::new())
-        };
-
-        if let Some(ref mut tile_cache) = state.tile_cache {
-            self.pictures[pic_index.0].update_prim_dependencies(
-                tile_cache,
-                frame_context,
-                resource_cache,
-                resources,
-                &self.pictures,
-                clip_store,
-                &self.opacity_bindings,
-                &self.images,
-            );
-        }
-
-        for child_pic_index in &children {
-            self.update_tile_cache(
-                *child_pic_index,
-                state,
-                frame_context,
-                resource_cache,
-                resources,
-                clip_store,
-                surfaces,
-                gpu_cache,
-                retained_tiles,
-                scratch,
-            );
-        }
-
         let pic = &mut self.pictures[pic_index.0];
+
         if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) = pic.raster_config {
-            let mut tile_cache = state.tile_cache.take().unwrap();
+            let mut tile_cache = frame_state.tile_cache.take().unwrap();
 
             // Build the dirty region(s) for this tile cache.
             pic.local_clip_rect = tile_cache.post_update(
-                resource_cache,
-                gpu_cache,
+                frame_state.resource_cache,
+                frame_state.gpu_cache,
                 frame_context,
-                scratch,
+                frame_state.scratch,
             );
 
             pic.tile_cache = Some(tile_cache);
         }
 
-        pic.prim_list.pictures = children;
+        pic.prim_list = prim_list;
     }
 
     pub fn get_opacity_binding(
         &self,
         opacity_binding_index: OpacityBindingIndex,
     ) -> f32 {
         if opacity_binding_index == OpacityBindingIndex::INVALID {
             1.0
@@ -2378,16 +2350,35 @@ impl PrimitiveStore {
         resources: &mut FrameResources,
         scratch: &mut PrimitiveScratchBuffer,
     ) {
         for (plane_split_anchor, prim_instance) in prim_list.prim_instances.iter_mut().enumerate() {
             if prim_instance.visibility_info == PrimitiveVisibilityIndex::INVALID {
                 continue;
             }
 
+            // The original clipped world rect was calculated during the initial visibility pass.
+            // 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];
+
+                match visibility_info.clipped_world_rect.intersection(&pic_context.dirty_world_rect) {
+                    Some(rect) => {
+                        visibility_info.clipped_world_rect = rect;
+                    }
+                    None => {
+                        prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
+                        continue;
+                    }
+                }
+            }
+
             let spatial_node = &frame_context
                 .clip_scroll_tree
                 .spatial_nodes[prim_instance.spatial_node_index.0 as usize];
 
             // TODO(gw): Although constructing these is cheap, they are often
             //           the same for many consecutive primitives, so it may
             //           be worth caching the most recent context.
             let prim_context = PrimitiveContext::new(
@@ -3078,17 +3069,16 @@ impl<'a> GpuDataRequest<'a> {
         }
 
         false
     }
 
 impl PrimitiveInstance {
     fn build_segments_if_needed(
         &mut self,
-        prim_local_clip_rect: LayoutRect,
         prim_clip_chain: &ClipChainInstance,
         frame_state: &mut FrameBuildingState,
         prim_store: &mut PrimitiveStore,
         resources: &FrameResources,
         segments_store: &mut SegmentStorage,
         segment_instances_store: &mut SegmentInstanceStorage,
     ) {
         let prim_data = &resources.as_common_data(self);
@@ -3129,17 +3119,17 @@ impl PrimitiveInstance {
             }
         };
 
         if *segment_instance_index == SegmentInstanceIndex::INVALID {
             let mut segments: SmallVec<[BrushSegment; 8]> = SmallVec::new();
 
             if write_brush_segment_description(
                 prim_local_rect,
-                prim_local_clip_rect,
+                self.local_clip_rect,
                 prim_clip_chain,
                 &mut frame_state.segment_builder,
                 frame_state.clip_store,
                 resources,
             ) {
                 frame_state.segment_builder.build(|segment| {
                     segments.push(
                         BrushSegment::new(
@@ -3166,17 +3156,16 @@ impl PrimitiveInstance {
                 *segment_instance_index = segment_instances_store.push(instance);
             };
         }
     }
 
     fn update_clip_task_for_brush(
         &self,
         prim_info: &mut PrimitiveVisibility,
-        prim_local_clip_rect: LayoutRect,
         root_spatial_node_index: SpatialNodeIndex,
         prim_context: &PrimitiveContext,
         pic_context: &PictureContext,
         pic_state: &mut PictureState,
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
         prim_store: &PrimitiveStore,
         resources: &mut FrameResources,
@@ -3289,17 +3278,17 @@ impl PrimitiveInstance {
                 let segment_clip_chain = frame_state
                     .clip_store
                     .build_clip_chain_instance(
                         self,
                         segment.local_rect.translate(&LayoutVector2D::new(
                             self.prim_origin.x,
                             self.prim_origin.y,
                         )),
-                        prim_local_clip_rect,
+                        self.local_clip_rect,
                         prim_context.spatial_node_index,
                         &pic_state.map_local_to_pic,
                         &pic_state.map_pic_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_context.device_pixel_scale,
                         &pic_context.dirty_world_rect,
@@ -3337,35 +3326,28 @@ impl PrimitiveInstance {
         scratch: &mut PrimitiveScratchBuffer,
     ) {
         let prim_info = &mut scratch.prim_info[self.visibility_info.0 as usize];
 
         if self.is_chased() {
             println!("\tupdating clip task with pic rect {:?}", prim_info.clip_chain.pic_clip_rect);
         }
 
-        let prim_clip_rect = resources
-            .as_common_data(self)
-            .prim_relative_clip_rect
-            .translate(&self.prim_origin.to_vector());
-
         self.build_segments_if_needed(
-            prim_clip_rect,
             &prim_info.clip_chain,
             frame_state,
             prim_store,
             resources,
             &mut scratch.segments,
             &mut scratch.segment_instances,
         );
 
         // First try to  render this primitive's mask using optimized brush rendering.
         if self.update_clip_task_for_brush(
             prim_info,
-            prim_clip_rect,
             root_spatial_node_index,
             prim_context,
             pic_context,
             pic_state,
             frame_context,
             frame_state,
             prim_store,
             resources,
@@ -3512,15 +3494,15 @@ fn update_opacity_binding(
 fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
-    assert_eq!(mem::size_of::<PrimitiveInstance>(), 80, "PrimitiveInstance size changed");
+    assert_eq!(mem::size_of::<PrimitiveInstance>(), 96, "PrimitiveInstance size changed");
     assert_eq!(mem::size_of::<PrimitiveInstanceKind>(), 40, "PrimitiveInstanceKind size changed");
-    assert_eq!(mem::size_of::<PrimitiveTemplate>(), 56, "PrimitiveTemplate size changed");
+    assert_eq!(mem::size_of::<PrimitiveTemplate>(), 40, "PrimitiveTemplate size changed");
     assert_eq!(mem::size_of::<PrimitiveTemplateKind>(), 20, "PrimitiveTemplateKind size changed");
-    assert_eq!(mem::size_of::<PrimitiveKey>(), 36, "PrimitiveKey size changed");
+    assert_eq!(mem::size_of::<PrimitiveKey>(), 20, "PrimitiveKey size changed");
     assert_eq!(mem::size_of::<PrimitiveKeyKind>(), 5, "PrimitiveKeyKind size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/picture.rs
+++ b/gfx/wr/webrender/src/prim_store/picture.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{
-    ColorU, FilterOp, LayoutRect, LayoutSize, LayoutPrimitiveInfo, MixBlendMode,
+    ColorU, FilterOp, LayoutSize, LayoutPrimitiveInfo, MixBlendMode,
     PropertyBinding, PropertyBindingId,
 };
 use app_units::Au;
 use display_list_flattener::{AsInstanceKind, IsVisible};
 use intern::{DataStore, Handle, Internable, Interner, InternDebug, UpdateList};
 use picture::PictureCompositeMode;
 use prim_store::{
     PrimKey, PrimKeyCommonData, PrimTemplate, PrimTemplateCommonData,
@@ -133,25 +133,23 @@ pub struct Picture {
 }
 
 pub type PictureKey = PrimKey<Picture>;
 
 impl PictureKey {
     pub fn new(
         is_backface_visible: bool,
         prim_size: LayoutSize,
-        prim_relative_clip_rect: LayoutRect,
         pic: Picture,
     ) -> Self {
 
         PictureKey {
             common: PrimKeyCommonData {
                 is_backface_visible,
                 prim_size: prim_size.into(),
-                prim_relative_clip_rect: prim_relative_clip_rect.into(),
             },
             kind: pic,
         }
     }
 }
 
 impl InternDebug for PictureKey {}
 
@@ -202,22 +200,20 @@ impl Internable for Picture {
     type Source = PictureKey;
     type StoreData = PictureTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> PictureKey {
         PictureKey::new(
             info.is_backface_visible,
             info.rect.size,
-            prim_relative_clip_rect,
             self
         )
     }
 }
 
 impl IsVisible for Picture {
     fn is_visible(&self) -> bool {
         true
@@ -230,11 +226,11 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<Picture>(), 84, "Picture size changed");
-    assert_eq!(mem::size_of::<PictureTemplate>(), 36, "PictureTemplate size changed");
-    assert_eq!(mem::size_of::<PictureKey>(), 112, "PictureKey size changed");
+    assert_eq!(mem::size_of::<PictureTemplate>(), 20, "PictureTemplate size changed");
+    assert_eq!(mem::size_of::<PictureKey>(), 96, "PictureKey size changed");
 }
--- a/gfx/wr/webrender/src/prim_store/text_run.rs
+++ b/gfx/wr/webrender/src/prim_store/text_run.rs
@@ -1,14 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ColorF, DevicePixelScale, GlyphInstance, LayoutPrimitiveInfo};
-use api::{LayoutRect, LayoutToWorldTransform, RasterSpace};
+use api::{LayoutToWorldTransform, RasterSpace};
 use api::{LayoutVector2D, Shadow};
 use display_list_flattener::{AsInstanceKind, CreateShadow, IsVisible};
 use frame_builder::{FrameBuildingState, PictureContext};
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::GpuCache;
 use intern;
 use prim_store::{PrimitiveOpacity, PrimitiveSceneData,  PrimitiveScratchBuffer};
 use prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData, VectorKey};
@@ -30,23 +30,21 @@ pub struct TextRunKey {
     pub offset: VectorKey,
     pub glyphs: Vec<GlyphInstance>,
     pub shadow: bool,
 }
 
 impl TextRunKey {
     pub fn new(
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
         text_run: TextRun,
     ) -> Self {
         TextRunKey {
             common: PrimKeyCommonData::with_info(
                 info,
-                prim_relative_clip_rect,
             ),
             font: text_run.font,
             offset: text_run.offset.into(),
             glyphs: text_run.glyphs,
             shadow: text_run.shadow,
         }
     }
 }
@@ -183,21 +181,19 @@ impl intern::Internable for TextRun {
     type Source = TextRunKey;
     type StoreData = TextRunTemplate;
     type InternData = PrimitiveSceneData;
 
     /// Build a new key from self with `info`.
     fn build_key(
         self,
         info: &LayoutPrimitiveInfo,
-        prim_relative_clip_rect: LayoutRect,
     ) -> TextRunKey {
         TextRunKey::new(
             info,
-            prim_relative_clip_rect,
             self,
         )
     }
 }
 
 impl CreateShadow for TextRun {
     fn create_shadow(&self, shadow: &Shadow) -> Self {
         let mut font = FontInstance {
@@ -343,12 +339,12 @@ fn test_struct_sizes() {
     use std::mem;
     // The sizes of these structures are critical for performance on a number of
     // talos stress tests. If you get a failure here on CI, there's two possibilities:
     // (a) You made a structure smaller than it currently is. Great work! Update the
     //     test expectations and move on.
     // (b) You made a structure larger. This is not necessarily a problem, but should only
     //     be done with care, and after checking if talos performance regresses badly.
     assert_eq!(mem::size_of::<TextRun>(), 112, "TextRun size changed");
-    assert_eq!(mem::size_of::<TextRunTemplate>(), 144, "TextRunTemplate size changed");
-    assert_eq!(mem::size_of::<TextRunKey>(), 136, "TextRunKey size changed");
+    assert_eq!(mem::size_of::<TextRunTemplate>(), 128, "TextRunTemplate size changed");
+    assert_eq!(mem::size_of::<TextRunKey>(), 120, "TextRunKey size changed");
     assert_eq!(mem::size_of::<TextRunPrimitive>(), 88, "TextRunPrimitive size changed");
 }
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -600,16 +600,17 @@ 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() {
             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_tiles(retained_tiles);
 
         self.frame_builder = Some(built_scene.frame_builder);