Bug 1511664 - Update webrender to commit 6ca634197cfe26738194f87042020fe838c0047a (WR PR #3379). r=kats
authorWR Updater Bot <graphics-team@mozilla.staktrace.com>
Sun, 02 Dec 2018 00:43:40 +0000
changeset 508333 3be4f687430ae11fe0131245a09f71f8bd879108
parent 508332 1ff8628621d03f4dc89d8b098208bbf2ecc9e479
child 508334 6060cddc780cb1e461841c1ad5ee363c6cd1470f
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1511664
milestone65.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 1511664 - Update webrender to commit 6ca634197cfe26738194f87042020fe838c0047a (WR PR #3379). r=kats https://github.com/servo/webrender/pull/3379 Differential Revision: https://phabricator.services.mozilla.com/D13630
gfx/webrender_bindings/revision.txt
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/renderer.rs
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-92235d1fc4ff4c4fb4e1ed709eb3136e96c786b5
+6ca634197cfe26738194f87042020fe838c0047a
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -25,18 +25,18 @@ use picture::{Picture3DContext, PictureC
 use prim_store::{PrimitiveInstance, PrimitiveDataInterner, PrimitiveKeyKind, RadialGradientParams};
 use prim_store::{PrimitiveKey, PrimitiveSceneData, PrimitiveInstanceKind, GradientStopKey, NinePatchDescriptor};
 use prim_store::{PrimitiveDataHandle, PrimitiveStore, PrimitiveStoreStats, LineDecorationCacheKey};
 use prim_store::{ScrollNodeAndClipChain, PictureIndex, register_prim_chase_id, get_line_decoration_sizes};
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, ScenePipeline, StackingContextHelpers};
 use scene_builder::DocumentResources;
-use spatial_node::{StickyFrameInfo, ScrollFrameKind};
-use std::{f32, mem};
+use spatial_node::{StickyFrameInfo, ScrollFrameKind, SpatialNodeType};
+use std::{f32, mem, usize};
 use std::collections::vec_deque::VecDeque;
 use tiling::{CompositeOps};
 use util::{MaxRect, VecHelper};
 
 #[derive(Debug, Copy, Clone)]
 struct ClipNode {
     id: ClipChainId,
     count: usize,
@@ -149,16 +149,27 @@ pub struct DisplayListFlattener<'a> {
 
     /// Reference to the document resources, which contains
     /// shared (interned) data between display lists.
     resources: &'a mut DocumentResources,
 
     /// The root picture index for this flattener. This is the picture
     /// to start the culling phase from.
     pub root_pic_index: PictureIndex,
+
+    /// TODO(gw): This is a complete hack that relies on knowledge of
+    ///           what the Gecko display list looks like. It's used
+    ///           for now to work out which scroll root to use to
+    ///           create the picture cache for the content. It's only
+    ///           ever used if picture caching is enabled in the
+    ///           RendererOptions struct. We will need to work out
+    ///           a better API to avoid this, before we enable it
+    ///           for all users. Another alternative is that this
+    ///           will disappear itself when document splitting is used.
+    picture_cache_scroll_root: Option<SpatialNodeIndex>,
 }
 
 impl<'a> DisplayListFlattener<'a> {
     pub fn create_frame_builder(
         scene: &Scene,
         clip_scroll_tree: &mut ClipScrollTree,
         font_instances: FontInstanceMap,
         view: &DocumentView,
@@ -186,43 +197,208 @@ impl<'a> DisplayListFlattener<'a> {
             hit_testing_runs: Vec::new(),
             pending_shadow_items: VecDeque::new(),
             sc_stack: Vec::new(),
             pipeline_clip_chain_stack: vec![ClipChainId::NONE],
             prim_store: PrimitiveStore::new(&prim_store_stats),
             clip_store: ClipStore::new(),
             resources,
             root_pic_index: PictureIndex(0),
+            picture_cache_scroll_root: None,
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
 
         flattener.flatten_root(
             root_pipeline,
             &root_pipeline.viewport_size,
         );
 
         debug_assert!(flattener.sc_stack.is_empty());
 
+        // If picture caching is enabled, splice up the root
+        // stacking context to enable correct surface caching.
+        flattener.setup_picture_caching(
+            root_pipeline_id,
+        );
+
         new_scene.root_pipeline_id = Some(root_pipeline_id);
         new_scene.pipeline_epochs = scene.pipeline_epochs.clone();
         new_scene.pipelines = scene.pipelines.clone();
 
         FrameBuilder::with_display_list_flattener(
             view.inner_rect,
             background_color,
             view.window_size,
             flattener,
         )
     }
 
+    /// Cut the primitives in the root stacking context based on the picture
+    /// caching scroll root. This is a temporary solution for the initial
+    /// implementation of picture caching. We need to work out the specifics
+    /// of how WR should decide (or Gecko should communicate) where the main
+    /// content frame is that should get surface caching.
+    fn setup_picture_caching(
+        &mut self,
+        root_pipeline_id: PipelineId,
+    ) {
+        if !self.config.enable_picture_caching {
+            return;
+        }
+
+        // This method is basically a hack to set up picture caching in a minimal
+        // way without having to check the public API (yet). The intent is to
+        // work out a good API for this and switch to using it. In the mean
+        // time, this allows basic picture caching to be enabled and used for
+        // ironing out remaining bugs, fixing performance issues and profiling.
+
+        //
+        // We know that the display list will contain something like the following:
+        //  [Some number of primitives attached to root scroll now]
+        //  [IFrame for the content]
+        //  [A scroll root for the content (what we're interested in)]
+        //  [Primitives attached to the scroll root, possibly with sub-scroll roots]
+        //  [Some number of trailing primitives attached to root scroll frame]
+        //
+        // So we want to slice that stacking context up into:
+        //  [root primitives]
+        //  [tile cache picture]
+        //     [primitives attached to cached scroll root]
+        //  [trailing root primitives]
+        //
+        // This step is typically very quick, because there are only
+        // a small number of items in the root stacking context, since
+        // most of the content is embedded in its own picture.
+        //
+
+        // See if we found a scroll root for the cached surface root.
+        if let Some(picture_cache_scroll_root) = self.picture_cache_scroll_root {
+            // Get the list of existing primitives in the main stacking context.
+            let mut old_prim_list = mem::replace(
+                &mut self.prim_store.pictures[self.root_pic_index.0].prim_list,
+                PrimitiveList::empty(),
+            );
+
+            // Find the first primitive which has the desired scroll root.
+            let first_index = old_prim_list.prim_instances.iter().position(|instance| {
+                let scroll_root = self.find_scroll_root(
+                    instance.spatial_node_index,
+                );
+
+                scroll_root == picture_cache_scroll_root
+            }).unwrap_or(old_prim_list.prim_instances.len());
+
+            // Split off the preceding primtives.
+            let mut remaining_prims = old_prim_list.prim_instances.split_off(first_index);
+
+            // Find the first primitive in reverse order that is not the root scroll node.
+            let last_index = remaining_prims.iter().rposition(|instance| {
+                let scroll_root = self.find_scroll_root(
+                    instance.spatial_node_index,
+                );
+
+                scroll_root != ROOT_SPATIAL_NODE_INDEX
+            }).unwrap_or(remaining_prims.len() - 1);
+
+            let preceding_prims = old_prim_list.prim_instances;
+            let trailing_prims = remaining_prims.split_off(last_index + 1);
+
+            let prim_list = PrimitiveList::new(
+                remaining_prims,
+                &self.resources.prim_interner,
+            );
+
+            // Now, create a picture with tile caching enabled that will hold all
+            // of the primitives selected as belonging to the main scroll root.
+            let prim_key = PrimitiveKey::new(
+                true,
+                LayoutRect::zero(),
+                LayoutRect::max_rect(),
+                PrimitiveKeyKind::Unused,
+            );
+
+            let primitive_data_handle = self.resources
+                .prim_interner
+                .intern(&prim_key, || {
+                    PrimitiveSceneData {
+                        culling_rect: LayoutRect::zero(),
+                        is_backface_visible: true,
+                    }
+                }
+            );
+
+            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,
+                root_pipeline_id,
+                None,
+                true,
+                RasterSpace::Screen,
+                prim_list,
+                picture_cache_scroll_root,
+                LayoutRect::max_rect(),
+                &self.clip_store,
+            ));
+
+            let instance = PrimitiveInstance::new(
+                PrimitiveInstanceKind::Picture { pic_index: PictureIndex(pic_index) },
+                primitive_data_handle,
+                ClipChainId::NONE,
+                picture_cache_scroll_root,
+            );
+
+            // This contains the tile caching picture, with preceding and
+            // trailing primitives outside the main scroll root.
+            let mut new_prim_list = preceding_prims;
+            new_prim_list.push(instance);
+            new_prim_list.extend(trailing_prims);
+
+            // Finally, store the sliced primitive list in the root picture.
+            self.prim_store.pictures[self.root_pic_index.0].prim_list = PrimitiveList::new(
+                new_prim_list,
+                &self.resources.prim_interner,
+            );
+        }
+    }
+
+    /// Find the spatial node that is the scroll root for a given
+    /// spatial node.
+    fn find_scroll_root(
+        &self,
+        spatial_node_index: SpatialNodeIndex,
+    ) -> SpatialNodeIndex {
+        let mut scroll_root = ROOT_SPATIAL_NODE_INDEX;
+        let mut node_index = spatial_node_index;
+
+        while node_index != ROOT_SPATIAL_NODE_INDEX {
+            let node = &self.clip_scroll_tree.spatial_nodes[node_index.0];
+            match node.node_type {
+                SpatialNodeType::ReferenceFrame(..) |
+                SpatialNodeType::StickyFrame(..) => {
+                    // TODO(gw): In future, we may need to consider sticky frames.
+                }
+                SpatialNodeType::ScrollFrame(ref info) => {
+                    // If we found an explicit scroll root, store that
+                    // and keep looking up the tree.
+                    if let ScrollFrameKind::Explicit = info.frame_kind {
+                        scroll_root = node_index;
+                    }
+                }
+            }
+            node_index = node.parent.expect("unable to find parent node");
+        }
+
+        scroll_root
+    }
+
     fn get_complex_clips(
         &self,
         pipeline_id: PipelineId,
         complex_clips: ItemRange<ComplexClipRegion>,
     ) -> impl 'a + Iterator<Item = ComplexClipRegion> {
         //Note: we could make this a bit more complex to early out
         // on `complex_clips.is_empty()` if it's worth it
         self.scene
@@ -369,26 +545,34 @@ impl<'a> DisplayListFlattener<'a> {
         // positioning offsets.
         let frame_rect = item.clip_rect().translate(reference_frame_relative_offset);
         let content_rect = item.rect().translate(reference_frame_relative_offset);
 
         debug_assert!(info.clip_id != info.scroll_frame_id);
 
         self.add_clip_node(info.clip_id, clip_and_scroll_ids, clip_region);
 
-        self.add_scroll_frame(
+        let node_index = self.add_scroll_frame(
             info.scroll_frame_id,
             info.clip_id,
             info.external_id,
             pipeline_id,
             &frame_rect,
             &content_rect.size,
             info.scroll_sensitivity,
             ScrollFrameKind::Explicit,
         );
+
+        // TODO(gw): See description of picture_cache_scroll_root field for information
+        //           about this temporary hack. What it's trying to identify is the first
+        //           scroll root within the first iframe that we encounter in the display
+        //           list.
+        if self.picture_cache_scroll_root.is_none() && pipeline_id != self.scene.root_pipeline_id.unwrap() {
+            self.picture_cache_scroll_root = Some(node_index);
+        }
     }
 
     fn flatten_reference_frame(
         &mut self,
         traversal: &mut BuiltDisplayListIter<'a>,
         pipeline_id: PipelineId,
         item: &DisplayItemRef,
         reference_frame: &ReferenceFrame,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -48,16 +48,17 @@ impl Default for ChasePrimitive {
 #[derive(Clone, Copy)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct FrameBuilderConfig {
     pub default_font_render_mode: FontRenderMode,
     pub dual_source_blending_is_supported: bool,
     pub dual_source_blending_is_enabled: bool,
     pub chase_primitive: ChasePrimitive,
+    pub enable_picture_caching: bool,
 }
 
 /// A builder structure for `tiling::Frame`
 pub struct FrameBuilder {
     screen_rect: DeviceIntRect,
     background_color: Option<ColorF>,
     window_size: DeviceIntSize,
     root_pic_index: PictureIndex,
@@ -149,16 +150,17 @@ impl FrameBuilder {
             background_color: None,
             root_pic_index: PictureIndex(0),
             pending_retained_tiles: FastHashMap::default(),
             config: FrameBuilderConfig {
                 default_font_render_mode: FontRenderMode::Mono,
                 dual_source_blending_is_enabled: true,
                 dual_source_blending_is_supported: false,
                 chase_primitive: ChasePrimitive::Nothing,
+                enable_picture_caching: false,
             },
         }
     }
 
     /// Provide any cached surface tiles from the previous frame builder
     /// to a new frame builder. These will be consumed or dropped the
     /// first time a new frame builder creates a frame.
     pub fn set_retained_tiles(
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -72,17 +72,18 @@ pub struct TileTransformInfo {
     /// Tiles check this to see if the dependencies have changed.
     changed: bool,
 }
 
 #[derive(Debug)]
 pub struct GlobalTransformInfo {
     /// Current (quantized) value of the transform, that is
     /// independent of the value of the spatial node index.
-    key: TransformKey,
+    /// Only calculated on first use.
+    current: Option<TransformKey>,
     /// Tiles check this to see if the dependencies have changed.
     changed: bool,
 }
 
 /// Information about the state of an opacity binding.
 #[derive(Debug)]
 pub struct OpacityBindingInfo {
     /// The current value retrieved from dynamic scene properties.
@@ -126,39 +127,55 @@ pub struct Tile {
     transform_info: Vec<TileTransformInfo>,
     /// Uniquely describes the content of this tile, in a way that can be
     /// (reasonably) efficiently hashed and compared.
     descriptor: TileDescriptor,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
-    fn new(tile_offset: TileOffset) -> Self {
+    fn new(
+        tile_offset: TileOffset,
+        local_tile_size: SizeKey,
+        raster_transform: TransformKey,
+    ) -> Self {
         Tile {
             opacity_bindings: FastHashSet::default(),
             image_keys: FastHashSet::default(),
             is_valid: false,
             is_visible: false,
             is_cacheable: true,
             in_use: false,
             handle: TextureCacheHandle::invalid(),
-            descriptor: TileDescriptor::new(tile_offset),
+            descriptor: TileDescriptor::new(
+                tile_offset,
+                local_tile_size,
+                raster_transform,
+            ),
             tile_transform_map: FastHashMap::default(),
             transform_info: Vec::new(),
         }
     }
 
     /// Add a (possibly) new transform dependency to this tile.
     fn push_transform_dependency(
         &mut self,
         spatial_node_index: SpatialNodeIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
-        global_transforms: &[GlobalTransformInfo],
+        global_transforms: &mut [GlobalTransformInfo],
     ) {
+        // If the primitive is positioned by the same spatial
+        // node as the surface, we don't care about it since
+        // the primitive can never move to a different position
+        // relative to the surface.
+        if spatial_node_index == surface_spatial_node_index {
+            return;
+        }
+
         let transform_info = &mut self.transform_info;
         let descriptor = &mut self.descriptor;
 
         // Get the mapping from unstable spatial node index to
         // a local transform index within this tile.
         let tile_transform_index = self
             .tile_transform_map
             .entry(spatial_node_index)
@@ -166,18 +183,27 @@ impl Tile {
                 let index = transform_info.len();
 
                 let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
                     surface_spatial_node_index,
                     spatial_node_index,
                     clip_scroll_tree,
                 ).expect("todo: handle invalid mappings");
 
+                // See if the transform changed, and cache the current
+                // transform if not set before.
+                let changed = get_global_transform_changed(
+                    global_transforms,
+                    spatial_node_index,
+                    clip_scroll_tree,
+                    surface_spatial_node_index,
+                );
+
                 transform_info.push(TileTransformInfo {
-                    changed: global_transforms[spatial_node_index.0].changed,
+                    changed,
                     spatial_node_index,
                 });
 
                 let key = mapping.into();
 
                 descriptor.transforms.push(key);
 
                 TileTransformIndex(index as u32)
@@ -234,26 +260,30 @@ pub struct TileDescriptor {
 
     /// Identifies the raster configuration of the rasterization
     /// root, to ensure tiles are invalidated if they are drawn in
     /// screen-space with an incompatible transform.
     pub raster_transform: TransformKey,
 }
 
 impl TileDescriptor {
-    fn new(tile_offset: TileOffset) -> Self {
+    fn new(
+        tile_offset: TileOffset,
+        local_tile_size: SizeKey,
+        raster_transform: TransformKey,
+    ) -> Self {
         TileDescriptor {
             prim_uids: Vec::new(),
             clip_uids: Vec::new(),
             transform_ids: Vec::new(),
             opacity_bindings: Vec::new(),
             transforms: Vec::new(),
             tile_offset,
-            raster_transform: TransformKey::Local,
-            local_tile_size: SizeKey::zero(),
+            raster_transform,
+            local_tile_size,
         }
     }
 
     /// Clear the dependency information for a tile, when the dependencies
     /// are being rebuilt.
     fn clear(&mut self) {
         self.prim_uids.clear();
         self.clip_uids.clear();
@@ -295,16 +325,20 @@ pub struct TileCache {
     /// 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
     /// force the tile to re-rasterize if they change (e.g.
     /// images, transforms).
     pub needs_update: bool,
     /// If Some(..) the region that is dirty in this picture.
     pub dirty_region: Option<DirtyRegion>,
+    /// The current transform of the surface itself, to allow
+    /// invalidating tiles if the surface transform changes.
+    /// This is only relevant when raster_space == RasterSpace::Screen.
+    raster_transform: TransformKey,
 }
 
 impl TileCache {
     /// Construct a new tile cache.
     pub fn new() -> Self {
         TileCache {
             tiles: Vec::new(),
             old_tiles: FastHashMap::default(),
@@ -313,16 +347,17 @@ impl TileCache {
             transforms: Vec::new(),
             opacity_bindings: FastHashMap::default(),
             needs_update: true,
             dirty_region: None,
             space_mapper: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 PictureRect::zero(),
             ),
+            raster_transform: TransformKey::Local,
         }
     }
 
     /// Update the transforms array for this tile cache from the clip-scroll
     /// tree. This marks each transform as changed for later use during
     /// tile invalidation.
     pub fn update_transforms(
         &mut self,
@@ -364,39 +399,38 @@ impl TileCache {
         self.local_tile_size = local_tile_rect.size;
 
         // 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() {
-                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
-                    surface_spatial_node_index,
-                    SpatialNodeIndex(i),
-                    frame_context.clip_scroll_tree,
-                ).expect("todo: handle invalid mappings");
+                // 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(
+                        surface_spatial_node_index,
+                        SpatialNodeIndex(i),
+                        frame_context.clip_scroll_tree,
+                    ).expect("todo: handle invalid mappings");
 
-                let key = mapping.into();
-                transform.changed = transform.key != key;
-                transform.key = key;
+                    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 i in 0 .. frame_context.clip_scroll_tree.spatial_nodes.len() {
-                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
-                    surface_spatial_node_index,
-                    SpatialNodeIndex(i),
-                    frame_context.clip_scroll_tree,
-                ).expect("todo: handle invalid mappings");
-
+            for _ in 0 .. frame_context.clip_scroll_tree.spatial_nodes.len() {
                 self.transforms.push(GlobalTransformInfo {
-                    key: mapping.into(),
+                    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();
@@ -408,17 +442,17 @@ impl TileCache {
             };
             self.opacity_bindings.insert(*id, OpacityBindingInfo {
                 value: *value,
                 changed,
             });
         }
 
         // Update the state of the transform for compositing this picture.
-        let raster_transform = match raster_space {
+        self.raster_transform = match raster_space {
             RasterSpace::Screen => {
                 // In general cases, if we're rasterizing a picture in screen space, then the
                 // value of the surface spatial node will affect the contents of the picture
                 // itself. However, if the surface and raster spatial nodes are in the same
                 // coordinate system (which is the common case!) then we are effectively drawing
                 // in a local space anyway, so don't care about that transform for the purposes
                 // of validating the surface cache contents.
 
@@ -441,17 +475,17 @@ impl TileCache {
         };
 
         // 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.
         for tile in &mut self.tiles {
             tile.descriptor.local_tile_size = self.local_tile_size.into();
-            tile.descriptor.raster_transform = raster_transform.clone();
+            tile.descriptor.raster_transform = self.raster_transform.clone();
 
             debug_assert_eq!(tile.transform_info.len(), tile.descriptor.transforms.len());
             for (info, transform) in tile.transform_info.iter_mut().zip(tile.descriptor.transforms.iter_mut()) {
                 let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
                     surface_spatial_node_index,
                     info.spatial_node_index,
                     frame_context.clip_scroll_tree,
                 ).expect("todo: handle invalid mappings");
@@ -529,20 +563,31 @@ impl TileCache {
                 // the tile address. This saves invalidating existing tiles when we
                 // just resize the picture by adding / remove primitives.
                 let tx = x0 - self.tile_rect.origin.x + x;
                 let ty = y0 - self.tile_rect.origin.y + y;
                 let tile_offset = TileOffset::new(x + x0, y + y0);
 
                 let tile = if tx >= 0 && ty >= 0 && tx < self.tile_rect.size.width && ty < self.tile_rect.size.height {
                     let index = (ty * self.tile_rect.size.width + tx) as usize;
-                    mem::replace(&mut self.tiles[index], Tile::new(tile_offset))
+                    mem::replace(
+                        &mut self.tiles[index],
+                        Tile::new(
+                            tile_offset,
+                            self.local_tile_size.into(),
+                            self.raster_transform.clone(),
+                        )
+                    )
                 } else {
                     self.old_tiles.remove(&tile_offset).unwrap_or_else(|| {
-                        Tile::new(tile_offset)
+                        Tile::new(
+                            tile_offset,
+                            self.local_tile_size.into(),
+                            self.raster_transform.clone(),
+                        )
                     })
                 };
                 new_tiles.push(tile);
             }
         }
 
         self.tiles = new_tiles;
         self.tile_rect.origin = TileOffset::new(x0, y0);
@@ -565,17 +610,25 @@ impl TileCache {
         self.space_mapper.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
         let prim_data = &prim_data_store[prim_instance.prim_data_handle];
 
         // Map the primitive local rect into the picture space.
-        let rect = match self.space_mapper.map(&prim_data.prim_rect) {
+        // 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_data.prim_rect.intersection(&prim_data.clip_rect) {
+            Some(rect) => rect,
+            None => return,
+        };
+
+        let rect = match self.space_mapper.map(&culling_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.
@@ -698,26 +751,26 @@ impl TileCache {
                     tile.image_keys.insert(*image_key);
                 }
 
                 // Include the transform of the primitive itself.
                 tile.push_transform_dependency(
                     prim_instance.spatial_node_index,
                     surface_spatial_node_index,
                     clip_scroll_tree,
-                    &self.transforms,
+                    &mut self.transforms,
                 );
 
                 // Include the transforms of any relevant clip nodes for this primitive.
                 for clip_chain_spatial_node in &clip_chain_spatial_nodes {
                     tile.push_transform_dependency(
                         *clip_chain_spatial_node,
                         surface_spatial_node_index,
                         clip_scroll_tree,
-                        &self.transforms,
+                        &mut self.transforms,
                     );
                 }
 
                 // Include any opacity bindings this primitive depends on.
                 for id in &opacity_bindings {
                     if tile.opacity_bindings.insert(*id) {
                         tile.descriptor.opacity_bindings.push(*id);
                     }
@@ -738,19 +791,17 @@ impl TileCache {
         frame_context: &FrameBuildingContext,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         retained_tiles: &mut FastHashMap<TileDescriptor, TextureCacheHandle>,
     ) {
         self.needs_update = false;
 
         for (_, tile) in self.old_tiles.drain() {
-            if resource_cache.texture_cache.is_allocated(&tile.handle) {
-                resource_cache.texture_cache.mark_unused(&tile.handle);
-            }
+            resource_cache.texture_cache.mark_unused(&tile.handle);
         }
 
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
@@ -763,96 +814,98 @@ impl TileCache {
         let mut dirty_rect = PictureRect::zero();
 
         // Step through each tile and invalidate if the dependencies have changed.
         for y in 0 .. self.tile_rect.size.height {
             for x in 0 .. self.tile_rect.size.width {
                 let i = y * self.tile_rect.size.width + x;
                 let tile = &mut self.tiles[i as usize];
 
+                // If this tile is unused (has no primitives on it), we can just
+                // skip any invalidation / dirty region work for it.
+                if !tile.in_use {
+                    continue;
+                }
+
+                let tile_rect = PictureRect::new(
+                    PicturePoint::new(
+                        (self.tile_rect.origin.x + x) as f32 * self.local_tile_size.width,
+                        (self.tile_rect.origin.y + y) as f32 * self.local_tile_size.height,
+                    ),
+                    self.local_tile_size,
+                );
+
+                // Check if this tile is actually visible.
+                let tile_world_rect = world_mapper
+                    .map(&tile_rect)
+                    .expect("bug: unable to map tile to world coords");
+                tile.is_visible = frame_context.screen_world_rect.intersects(&tile_world_rect);
+
                 // Try to reuse cached tiles from the previous scene in this new
                 // scene, if possible.
-                if !resource_cache.texture_cache.is_allocated(&tile.handle) {
+                if tile.is_visible && !resource_cache.texture_cache.is_allocated(&tile.handle) {
                     // See if we have a retained tile from last scene that matches the
                     // exact content of this tile.
-                    if let Some(handle) = retained_tiles.remove(&tile.descriptor) {
+                    if let Some(retained_handle) = retained_tiles.remove(&tile.descriptor) {
                         // Only use if not evicted from texture cache in the meantime.
-                        if resource_cache.texture_cache.is_allocated(&handle) {
+                        if resource_cache.texture_cache.is_allocated(&retained_handle) {
                             // We found a matching tile from the previous scene, so use it!
-                            tile.handle = handle;
+                            tile.handle = retained_handle;
                             tile.is_valid = true;
                             // We know that the hash key of the descriptor validates that
                             // the local transforms in this tile exactly match the value
                             // of the current relative transforms needed for this tile,
                             // so we can mark those transforms as valid to avoid the
                             // retained tile being invalidated below.
                             for info in &mut tile.transform_info {
                                 info.changed = false;
                             }
                         }
                     }
                 }
 
-                let tile_rect = PictureRect::new(
-                    PicturePoint::new(
-                        (self.tile_rect.origin.x + x) as f32 * self.local_tile_size.width,
-                        (self.tile_rect.origin.y + y) as f32 * self.local_tile_size.height,
-                    ),
-                    self.local_tile_size,
-                );
-
                 // Invalidate the tile if not cacheable
                 if !tile.is_cacheable {
                     tile.is_valid = false;
                 }
 
-                if !tile.in_use {
-                    tile.is_valid = false;
-                }
-
                 // Invalidate the tile if any images have changed
                 for image_key in &tile.image_keys {
                     if resource_cache.is_image_dirty(*image_key) {
                         tile.is_valid = false;
                         break;
                     }
                 }
 
-                // Invalidate the tile if any dependent transforms changed
-                for info in &tile.transform_info {
-                    if info.changed {
-                        tile.is_valid = false;
-                        break;
-                    }
-                }
-
                 // Invalidate the tile if any opacity bindings changed.
                 for id in &tile.opacity_bindings {
                     let changed = match self.opacity_bindings.get(id) {
                         Some(info) => info.changed,
                         None => true,
                     };
                     if changed {
                         tile.is_valid = false;
                         break;
                     }
                 }
 
+                // Invalidate the tile if any dependent transforms changed
+                for info in &tile.transform_info {
+                    if info.changed {
+                        tile.is_valid = false;
+                        break;
+                    }
+                }
+
                 // Invalidate the tile if it was evicted by the texture cache.
                 if !resource_cache.texture_cache.is_allocated(&tile.handle) {
                     tile.is_valid = false;
                 }
 
-                // Check if this tile is actually visible.
-                let tile_world_rect = world_mapper
-                    .map(&tile_rect)
-                    .expect("bug: unable to map tile to world coords");
-                tile.is_visible = frame_context.screen_world_rect.intersects(&tile_world_rect);
-
-                if tile.is_visible && tile.in_use {
+                if tile.is_visible {
                     // Ensure we request the texture cache handle for this tile
                     // each frame it will be used so the texture cache doesn't
                     // decide to evict tiles that we currently want to use.
                     resource_cache.texture_cache.request(&tile.handle, gpu_cache);
 
                     // If we have an invalid tile, which is also visible, add it to the
                     // dirty rect we will need to draw.
                     if !tile.is_valid {
@@ -2115,30 +2168,30 @@ impl PicturePrimitive {
                         // Step through each tile and build the dirty rect
                         for y in 0 .. tile_cache.tile_rect.size.height {
                             for x in 0 .. tile_cache.tile_rect.size.width {
                                 let i = y * tile_cache.tile_rect.size.width + x;
                                 let tile = &mut tile_cache.tiles[i as usize];
 
                                 // If tile is invalidated, and on-screen, then we will
                                 // need to rasterize it.
-                                if !tile.is_valid && tile.is_visible {
+                                if !tile.is_valid && tile.is_visible && tile.in_use {
                                     // Notify the texture cache that we want to use this handle
                                     // and make sure it is allocated.
                                     frame_state.resource_cache.texture_cache.update(
                                         &mut tile.handle,
                                         descriptor,
                                         TextureFilter::Linear,
                                         None,
                                         [0.0; 3],
                                         DirtyRect::All,
                                         frame_state.gpu_cache,
                                         None,
                                         UvRectKind::Rect,
-                                        Eviction::Auto,
+                                        Eviction::Eager,
                                     );
 
                                     let cache_item = frame_state
                                         .resource_cache
                                         .get_texture_cache_item(&tile.handle);
 
                                     // Set up the blit command now that we know where the dest
                                     // rect is in the texture cache.
@@ -2615,8 +2668,33 @@ 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)
 }
+
+// Check whether a relative transform between two spatial nodes has changed
+// since last frame. If that relative transform hasn't been calculated, then
+// do that now and store it for later use.
+fn get_global_transform_changed(
+    global_transforms: &mut [GlobalTransformInfo],
+    spatial_node_index: SpatialNodeIndex,
+    clip_scroll_tree: &ClipScrollTree,
+    surface_spatial_node_index: SpatialNodeIndex,
+) -> bool {
+    let transform = &mut global_transforms[spatial_node_index.0];
+
+    if transform.current.is_none() {
+        let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
+            surface_spatial_node_index,
+            spatial_node_index,
+            clip_scroll_tree,
+        ).expect("todo: handle invalid mappings");
+
+        transform.current = Some(mapping.into());
+        transform.changed = true;
+    }
+
+    transform.changed
+}
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -1821,16 +1821,17 @@ impl Renderer {
             (false, _) => FontRenderMode::Mono,
         };
 
         let config = FrameBuilderConfig {
             default_font_render_mode,
             dual_source_blending_is_enabled: true,
             dual_source_blending_is_supported: ext_dual_source_blending,
             chase_primitive: options.chase_primitive,
+            enable_picture_caching: options.enable_picture_caching,
         };
 
         let device_pixel_ratio = options.device_pixel_ratio;
         // First set the flags to default and later call set_debug_flags to ensure any
         // potential transition when enabling a flag is run.
         let debug_flags = DebugFlags::default();
         let payload_rx_for_backend = payload_rx.to_mpsc_receiver();
         let recorder = options.recorder;