Bug 1510593 - Update webrender to commit 3d73e3885907ae3d48b46fba891073abdb59e76d (WR PR #3359). r=kats
authorWR Updater Bot <graphics-team@mozilla.staktrace.com>
Wed, 28 Nov 2018 14:08:00 +0000
changeset 507738 8692990356a7e39a67ee65909ff01f621d0830fb
parent 507737 3dcdcddd94ff74e05fb4793e970433772ac99098
child 507739 db270ae8003b5eaac4658768af0b5f63369a942a
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
bugs1510593
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 1510593 - Update webrender to commit 3d73e3885907ae3d48b46fba891073abdb59e76d (WR PR #3359). r=kats https://github.com/servo/webrender/pull/3359 Differential Revision: https://phabricator.services.mozilla.com/D13247
gfx/webrender_bindings/revision.txt
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/scene.rs
gfx/wr/webrender/src/surface.rs
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-323b7ea140ba956af342e4f2660e67e04913e9fb
+3d73e3885907ae3d48b46fba891073abdb59e76d
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -996,17 +996,17 @@ impl AlphaBatchBuilder {
                                 let tile_cache = picture.tile_cache.as_ref().unwrap();
 
                                 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 = &tile_cache.tiles[i as usize];
 
                                         // Check if the tile is visible.
-                                        if !tile.is_visible {
+                                        if !tile.is_visible || !tile.in_use {
                                             continue;
                                         }
 
                                         // Get the local rect of the tile.
                                         let tile_rect = LayoutRect::new(
                                             LayoutPoint::new(
                                                 (tile_cache.tile_rect.origin.x + x) as f32 * tile_cache.local_tile_size.width,
                                                 (tile_cache.tile_rect.origin.y + y) as f32 * tile_cache.local_tile_size.height,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -7,29 +7,30 @@ use api::{DeviceIntRect, DeviceIntSize, 
 use api::{LayoutPoint, LayoutRect, LayoutSize, PipelineId, RasterSpace, WorldPoint, WorldRect, WorldPixel};
 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::{PictureSurface, PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex, TileDescriptor};
 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};
 use segment::SegmentBuilder;
 use spatial_node::SpatialNode;
-use std::f32;
+use std::{f32, mem};
 use std::sync::Arc;
+use texture_cache::TextureCacheHandle;
 use tiling::{Frame, RenderPass, RenderPassKind, RenderTargetContext};
 use tiling::{SpecialRenderPasses};
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ChasePrimitive {
@@ -55,16 +56,19 @@ pub struct FrameBuilderConfig {
 }
 
 /// A builder structure for `tiling::Frame`
 pub struct FrameBuilder {
     screen_rect: DeviceIntRect,
     background_color: Option<ColorF>,
     window_size: DeviceIntSize,
     root_pic_index: PictureIndex,
+    /// Cache of surface tiles from the previous frame builder
+    /// that can optionally be consumed by this frame builder.
+    pending_retained_tiles: FastHashMap<TileDescriptor, TextureCacheHandle>,
     pub prim_store: PrimitiveStore,
     pub clip_store: ClipStore,
     pub hit_testing_runs: Vec<HitTestingRun>,
     pub config: FrameBuilderConfig,
 }
 
 pub struct FrameBuildingContext<'a> {
     pub device_pixel_scale: DevicePixelScale,
@@ -139,43 +143,67 @@ impl FrameBuilder {
         FrameBuilder {
             hit_testing_runs: Vec::new(),
             prim_store: PrimitiveStore::new(&PrimitiveStoreStats::empty()),
             clip_store: ClipStore::new(),
             screen_rect: DeviceIntRect::zero(),
             window_size: DeviceIntSize::zero(),
             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,
             },
         }
     }
 
+    /// 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(
+        &mut self,
+        retained_tiles: FastHashMap<TileDescriptor, TextureCacheHandle>,
+    ) {
+        debug_assert!(self.pending_retained_tiles.is_empty());
+        self.pending_retained_tiles = retained_tiles;
+    }
+
     pub fn with_display_list_flattener(
         screen_rect: DeviceIntRect,
         background_color: Option<ColorF>,
         window_size: DeviceIntSize,
         flattener: DisplayListFlattener,
     ) -> Self {
         FrameBuilder {
             hit_testing_runs: flattener.hit_testing_runs,
             prim_store: flattener.prim_store,
             clip_store: flattener.clip_store,
             root_pic_index: flattener.root_pic_index,
             screen_rect,
             background_color,
             window_size,
+            pending_retained_tiles: FastHashMap::default(),
             config: flattener.config,
         }
     }
 
+    /// 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 FastHashMap<TileDescriptor, TextureCacheHandle>,
+    ) {
+        self.prim_store.destroy(
+            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>>,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
@@ -222,31 +250,37 @@ impl FrameBuilder {
             ROOT_SPATIAL_NODE_INDEX,
             0.0,
             screen_world_rect,
             clip_scroll_tree,
         );
         surfaces.push(root_surface);
 
         let mut pic_update_state = PictureUpdateState::new(surfaces);
+        let mut retained_tiles = mem::replace(
+            &mut self.pending_retained_tiles,
+            FastHashMap::default(),
+        );
 
         // The first major pass of building a frame is to walk the picture
         // tree. This pass must be quick (it should never touch individual
         // primitives). For now, all we do here is determine which pictures
         // will create surfaces. In the future, this will be expanded to
         // set up render tasks, determine scaling of surfaces, and detect
         // which surfaces have valid cached surfaces that don't need to
         // be rendered this frame.
         self.prim_store.update_picture(
             self.root_pic_index,
             &mut pic_update_state,
             &frame_context,
             resource_cache,
+            gpu_cache,
             &resources.prim_data_store,
             &self.clip_store,
+            &mut retained_tiles,
         );
 
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -2,30 +2,30 @@
  * 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};
 use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
 use api::{DevicePixelScale, RasterRect, RasterSpace, PictureSize, DeviceIntPoint, ColorF, ImageKey, DirtyRect};
 use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor};
 use box_shadow::{BLUR_SAMPLE_SCALE};
-use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode};
+use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode, ClipUid};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use device::TextureFilter;
 use euclid::{TypedScale, vec3, TypedRect, TypedPoint2D, TypedSize2D};
 use euclid::approxeq::ApproxEq;
 use internal_types::{FastHashMap, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
-use gpu_cache::{GpuCacheAddress, GpuCacheHandle};
+use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{TransformPalette, TransformPaletteId, UvRectKind};
 use internal_types::FastHashSet;
 use plane_split::{Clipper, Polygon, Splitter};
-use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind};
+use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind, PrimitiveUid};
 use prim_store::{get_raster_rects, PrimitiveDataInterner, PrimitiveDataStore, CoordinateSpaceMapping};
-use prim_store::{OpacityBindingStorage, PrimitiveTemplateKind, ImageInstanceStorage};
+use prim_store::{OpacityBindingStorage, PrimitiveTemplateKind, ImageInstanceStorage, OpacityBindingIndex, SizeKey};
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
 use smallvec::SmallVec;
 use surface::{SurfaceDescriptor, TransformKey};
 use std::{mem, ops};
 use texture_cache::{Eviction, TextureCacheHandle};
@@ -60,68 +60,212 @@ pub type TileRect = TypedRect<i32, TileC
 
 /// The size in device pixels of a cached tile. The currently chosen
 /// size is arbitrary. We should do some profiling to find the best
 /// size for real world pages.
 pub const TILE_SIZE_DP: i32 = 512;
 
 /// Information about the state of a transform dependency.
 #[derive(Debug)]
-pub struct TransformInfo {
-    /// Quantized transform value
+pub struct TileTransformInfo {
+    /// The spatial node in the current clip-scroll tree that
+    /// this transform maps to.
+    spatial_node_index: SpatialNodeIndex,
+    /// 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,
     /// 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.
+    value: f32,
+    /// True if it was changed (or is new) since the last frame build.
+    changed: bool,
+}
+
 /// Information about a cached tile.
 #[derive(Debug)]
 pub struct Tile {
-    // TODO(gw): We could perhaps use a bitset here instead of a hash set?
-    /// The set of transform values that primitives in this tile depend on.
-    transforms: FastHashSet<SpatialNodeIndex>,
     /// The set of opacity bindings that this tile depends on.
-    opacity_bindings: FastHashMap<PropertyBindingId, f32>,
+    opacity_bindings: FastHashSet<PropertyBindingId>,
     /// Set of image keys that this tile depends on.
     image_keys: FastHashSet<ImageKey>,
     /// If true, this tile is marked valid, and the existing texture
     /// cache handle can be used. Tiles are invalidated during the
     /// build_dirty_regions method.
     is_valid: bool,
     /// If false, this tile cannot be cached (e.g. it has an external
     /// video image attached to it). In future, we could add an API
     /// for the embedder to tell us if the external image changed.
     /// This is set during primitive dependency updating.
     is_cacheable: bool,
     /// If true, this tile is currently in use by the cache. It
     /// may be false if the tile is outside the bounding rect of
     /// the current picture, but hasn't been discarded yet. This
     /// is calculated during primitive dependency updating.
-    in_use: bool,
+    pub in_use: bool,
     /// If true, this tile is currently visible on screen. This
     /// is calculated during build_dirty_regions.
     pub is_visible: bool,
     /// Handle to the cached texture for this tile.
     pub handle: TextureCacheHandle,
+    /// A map from clip-scroll tree spatial node indices to the tile
+    /// transforms. This allows the tile transforms to be stable
+    /// if the content of the tile is the same, but the shape of the
+    /// clip-scroll tree changes between scenes in other areas.
+    tile_transform_map: FastHashMap<SpatialNodeIndex, TileTransformIndex>,
+    /// Information about the transforms that is not part of the cache key.
+    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() -> Self {
+    fn new(tile_offset: TileOffset) -> Self {
         Tile {
-            transforms: FastHashSet::default(),
-            opacity_bindings: FastHashMap::default(),
+            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),
+            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],
+    ) {
+        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)
+            .or_insert_with(|| {
+                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");
+
+                transform_info.push(TileTransformInfo {
+                    changed: global_transforms[spatial_node_index.0].changed,
+                    spatial_node_index,
+                });
+
+                let key = mapping.into();
+
+                descriptor.transforms.push(key);
+
+                TileTransformIndex(index as u32)
+            });
+
+        // Record the transform for this primitive / clip node.
+        // TODO(gw): It might be worth storing these in runs, since they
+        //           probably don't change very often between prims.
+        descriptor.transform_ids.push(*tile_transform_index);
+    }
+
+    /// Destroy a tile, optionally returning a handle and cache descriptor,
+    /// if this surface was valid and may be useful on the next scene.
+    fn destroy(self) -> Option<(TileDescriptor, TextureCacheHandle)> {
+        if self.is_valid {
+            Some((self.descriptor, self.handle))
+        } else {
+            None
+        }
+    }
+}
+
+/// Index of a transform array local to the tile.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct TileTransformIndex(u32);
+
+/// Uniquely describes the content of this tile, in a way that can be
+/// (reasonably) efficiently hashed and compared.
+#[derive(Debug, Eq, PartialEq, Hash)]
+pub struct TileDescriptor {
+    /// List of primitive unique identifiers. The uid is guaranteed
+    /// to uniquely describe the content of the primitive.
+    pub prim_uids: Vec<PrimitiveUid>,
+
+    /// List of clip node unique identifiers. The uid is guaranteed
+    /// to uniquely describe the content of the clip node.
+    pub clip_uids: Vec<ClipUid>,
+
+    /// List of local tile transform ids that are used to position
+    /// the primitive and clip items above.
+    pub transform_ids: Vec<TileTransformIndex>,
+
+    /// List of transforms used by this tile, along with the current
+    /// quantized value.
+    pub transforms: Vec<TransformKey>,
+
+    /// The set of opacity bindings that this tile depends on.
+    // TODO(gw): Ugh, get rid of all opacity binding support!
+    pub opacity_bindings: Vec<PropertyBindingId>,
+
+    /// Ensures that we hash to a tile in the same local position.
+    pub tile_offset: TileOffset,
+    pub local_tile_size: SizeKey,
+
+    /// 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 {
+        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(),
+        }
+    }
+
+    /// 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();
+        self.transform_ids.clear();
+        self.transforms.clear();
+        self.opacity_bindings.clear();
+    }
 }
 
 /// Represents the dirty region of a tile cache picture.
 /// In future, we will want to support multiple dirty
 /// regions.
 #[derive(Debug)]
 pub struct DirtyRegion {
     tile_offset: DeviceIntPoint,
@@ -137,17 +281,20 @@ pub struct TileCache {
     pub tiles: Vec<Tile>,
     /// A set of tiles that were used last time we built
     /// the tile grid, that may be reused or discarded next time.
     pub old_tiles: FastHashMap<TileOffset, Tile>,
     /// The current size of the rect in tile coordinates.
     pub tile_rect: TileRect,
     /// List of transform keys - used to check if transforms
     /// have changed.
-    pub transforms: Vec<TransformInfo>,
+    pub transforms: Vec<GlobalTransformInfo>,
+    /// List of opacity bindings, with some extra information
+    /// about whether they changed since last frame.
+    pub opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
     /// A helper struct to map local rects into picture coords.
     pub space_mapper: SpaceMapper<LayoutPixel, PicturePixel>,
     /// 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,
@@ -159,31 +306,34 @@ impl TileCache {
     /// Construct a new tile cache.
     pub fn new() -> Self {
         TileCache {
             tiles: Vec::new(),
             old_tiles: FastHashMap::default(),
             tile_rect: TileRect::zero(),
             local_tile_size: PictureSize::zero(),
             transforms: Vec::new(),
+            opacity_bindings: FastHashMap::default(),
             needs_update: true,
             dirty_region: None,
             space_mapper: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 PictureRect::zero(),
             ),
         }
     }
 
     /// 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,
         surface_spatial_node_index: SpatialNodeIndex,
+        raster_spatial_node_index: SpatialNodeIndex,
+        raster_space: RasterSpace,
         frame_context: &FrameBuildingContext,
     ) {
         // Initialize the space mapper with current bounds,
         // which is used during primitive dependency updates.
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
             frame_context.screen_world_rect,
@@ -212,68 +362,127 @@ impl TileCache {
             .unmap(&world_tile_rect)
             .expect("bug: unable to get local tile size");
         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.
-        self.needs_update = if self.transforms.len() == frame_context.clip_scroll_tree.spatial_nodes.len() {
-            // If the transform array length is the same, then we can walk the list
-            // and check if the values of each transform are the same.
-            let mut any_transforms_changed = false;
-
+        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");
 
                 let key = mapping.into();
                 transform.changed = transform.key != key;
                 transform.key = key;
-
-                any_transforms_changed |= transform.changed;
             }
-
-            any_transforms_changed
         } 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");
 
-                self.transforms.push(TransformInfo {
+                self.transforms.push(GlobalTransformInfo {
                     key: mapping.into(),
                     changed: true,
                 });
             }
+        };
 
-            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),
+                None => true,
+            };
+            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 {
+            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.
+
+                let mut key = CoordinateSpaceMapping::<LayoutPixel, PicturePixel>::new(
+                    raster_spatial_node_index,
+                    surface_spatial_node_index,
+                    frame_context.clip_scroll_tree,
+                ).expect("bug: unable to get coord mapping").into();
+
+                if let TransformKey::ScaleOffset(ref mut key) = key {
+                    key.offset_x = 0.0;
+                    key.offset_y = 0.0;
+                }
+
+                key
+            }
+            RasterSpace::Local(..) => {
+                TransformKey::local()
+            }
         };
 
+        // 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();
+
+            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");
+                let new_transform = mapping.into();
+
+                info.changed = *transform != new_transform;
+                *transform = new_transform;
+
+                self.needs_update |= info.changed;
+            }
+        }
+
         // If we need to update the dependencies for tiles, walk each tile
         // and clear the transforms and opacity bindings arrays.
         if self.needs_update {
             debug_assert!(self.old_tiles.is_empty());
 
             // Clear any dependencies on the set of existing tiles, since
             // they are going to be rebuilt. Drain the tiles list and add
             // them to the old_tiles hash, for re-use next frame build.
             for (i, mut tile) in self.tiles.drain(..).enumerate() {
                 let y = i as i32 / self.tile_rect.size.width;
                 let x = i as i32 % self.tile_rect.size.width;
-                tile.transforms.clear();
+                tile.descriptor.clear();
+                tile.transform_info.clear();
+                tile.tile_transform_map.clear();
                 tile.opacity_bindings.clear();
                 tile.image_keys.clear();
                 tile.in_use = false;
                 let key = TileOffset::new(
                     x + self.tile_rect.origin.x,
                     y + self.tile_rect.origin.y,
                 );
                 self.old_tiles.insert(key, tile);
@@ -316,22 +525,25 @@ impl TileCache {
 
         for y in 0 .. y_tiles {
             for x in 0 .. x_tiles {
                 // See if we can re-use an existing tile from the old array, by mapping
                 // 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())
+                    mem::replace(&mut self.tiles[index], Tile::new(tile_offset))
                 } else {
-                    self.old_tiles.remove(&TileOffset::new(x + x0, y + y0)).unwrap_or_else(Tile::new)
+                    self.old_tiles.remove(&tile_offset).unwrap_or_else(|| {
+                        Tile::new(tile_offset)
+                    })
                 };
                 new_tiles.push(tile);
             }
         }
 
         self.tiles = new_tiles;
         self.tile_rect.origin = TileOffset::new(x0, y0);
         self.tile_rect.size = TileSize::new(x_tiles, y_tiles);
@@ -342,17 +554,16 @@ impl TileCache {
         &mut self,
         prim_instance: &PrimitiveInstance,
         surface_spatial_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
         prim_data_store: &PrimitiveDataStore,
         clip_chain_nodes: &[ClipChainNode],
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
-        scene_properties: &SceneProperties,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         self.space_mapper.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
@@ -377,53 +588,58 @@ impl TileCache {
         let y0 = (rect.origin.y / self.local_tile_size.height).floor() as i32;
         let x1 = ((rect.origin.x + rect.size.width) / self.local_tile_size.width).ceil() as i32;
         let y1 = ((rect.origin.y + rect.size.height) / self.local_tile_size.height).ceil() as i32;
 
         // Update the tile array allocation if needed.
         self.reconfigure_tiles_if_required(x0, y0, x1, y1);
 
         // Build the list of resources that this primitive has dependencies on.
-        let mut opacity_bindings: SmallVec<[(PropertyBindingId, f32); 4]> = SmallVec::new();
+        let mut opacity_bindings: SmallVec<[PropertyBindingId; 4]> = SmallVec::new();
         let mut clip_chain_spatial_nodes: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
+        let mut clip_chain_uids: SmallVec<[ClipUid; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
         let mut current_clip_chain_id = prim_instance.clip_chain_id;
 
         // Some primitives can not be cached (e.g. external video images)
         let is_cacheable = prim_instance.is_cacheable(
             prim_data_store,
             resource_cache,
         );
 
         match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index } => {
                 // Pictures can depend on animated opacity bindings.
                 let pic = &pictures[pic_index.0];
                 if let Some(PictureCompositeMode::Filter(FilterOp::Opacity(binding, _))) = pic.requested_composite_mode {
-                    if let PropertyBinding::Binding(key, default) = binding {
-                        opacity_bindings.push((key.id, default));
+                    if let PropertyBinding::Binding(key, _) = binding {
+                        opacity_bindings.push(key.id);
                     }
                 }
             }
             PrimitiveInstanceKind::Rectangle { opacity_binding_index, .. } => {
-                let opacity_binding = &opacity_binding_store[opacity_binding_index];
-                for binding in &opacity_binding.bindings {
-                    if let PropertyBinding::Binding(key, default) = binding {
-                        opacity_bindings.push((key.id, *default));
+                if opacity_binding_index != OpacityBindingIndex::INVALID {
+                    let opacity_binding = &opacity_binding_store[opacity_binding_index];
+                    for binding in &opacity_binding.bindings {
+                        if let PropertyBinding::Binding(key, _) = binding {
+                            opacity_bindings.push(key.id);
+                        }
                     }
                 }
             }
             PrimitiveInstanceKind::Image { image_instance_index, .. } => {
                 let image_instance = &image_instances[image_instance_index];
                 let opacity_binding_index = image_instance.opacity_binding_index;
 
-                let opacity_binding = &opacity_binding_store[opacity_binding_index];
-                for binding in &opacity_binding.bindings {
-                    if let PropertyBinding::Binding(key, default) = binding {
-                        opacity_bindings.push((key.id, *default));
+                if opacity_binding_index != OpacityBindingIndex::INVALID {
+                    let opacity_binding = &opacity_binding_store[opacity_binding_index];
+                    for binding in &opacity_binding.bindings {
+                        if let PropertyBinding::Binding(key, _) = binding {
+                            opacity_bindings.push(key.id);
+                        }
                     }
                 }
 
                 match prim_data.kind {
                     PrimitiveTemplateKind::Image { key, .. } => {
                         image_keys.push(key);
                     }
                     _ => {
@@ -447,31 +663,26 @@ impl TileCache {
             PrimitiveInstanceKind::NormalBorder { .. } |
             PrimitiveInstanceKind::LinearGradient { .. } |
             PrimitiveInstanceKind::RadialGradient { .. } |
             PrimitiveInstanceKind::ImageBorder { .. } => {
                 // These don't contribute dependencies
             }
         }
 
-        for (key, current) in &mut opacity_bindings {
-            if let Some(value) = scene_properties.get_float_value(*key) {
-                *current = value;
-            }
-        }
-
         // The transforms of any clips that are relative to the picture may affect
         // the content rendered by this primitive.
         while current_clip_chain_id != ClipChainId::NONE {
             let clip_chain_node = &clip_chain_nodes[current_clip_chain_id.0 as usize];
             // We only care about clip nodes that have transforms that are children
             // of the surface, since clips that are positioned by parents will be
             // handled by the clip collector when these tiles are composited.
             if clip_chain_node.spatial_node_index > surface_spatial_node_index {
                 clip_chain_spatial_nodes.push(clip_chain_node.spatial_node_index);
+                clip_chain_uids.push(clip_chain_node.handle.uid());
             }
             current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
         }
 
         // Normalize the tile coordinates before adding to tile dependencies.
         // For each affected tile, mark any of the primitive dependencies.
         for y in y0 - self.tile_rect.origin.y .. y1 - self.tile_rect.origin.y {
             for x in x0 - self.tile_rect.origin.x .. x1 - self.tile_rect.origin.x {
@@ -483,38 +694,56 @@ impl TileCache {
                 tile.in_use = true;
 
                 // Include any image keys this tile depends on.
                 for image_key in &image_keys {
                     tile.image_keys.insert(*image_key);
                 }
 
                 // Include the transform of the primitive itself.
-                tile.transforms.insert(prim_instance.spatial_node_index);
+                tile.push_transform_dependency(
+                    prim_instance.spatial_node_index,
+                    surface_spatial_node_index,
+                    clip_scroll_tree,
+                    &self.transforms,
+                );
 
                 // Include the transforms of any relevant clip nodes for this primitive.
                 for clip_chain_spatial_node in &clip_chain_spatial_nodes {
-                    tile.transforms.insert(*clip_chain_spatial_node);
+                    tile.push_transform_dependency(
+                        *clip_chain_spatial_node,
+                        surface_spatial_node_index,
+                        clip_scroll_tree,
+                        &self.transforms,
+                    );
                 }
 
                 // Include any opacity bindings this primitive depends on.
-                for &(id, value) in &opacity_bindings {
-                    tile.opacity_bindings.insert(id, value);
+                for id in &opacity_bindings {
+                    if tile.opacity_bindings.insert(*id) {
+                        tile.descriptor.opacity_bindings.push(*id);
+                    }
                 }
+
+                // Update the tile descriptor, used for tile comparison during scene swaps.
+                tile.descriptor.prim_uids.push(prim_instance.prim_data_handle.uid());
+                tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
             }
         }
     }
 
     /// Build the dirty region(s) for the tile cache after all primitive
     /// dependencies have been updated.
     pub fn build_dirty_regions(
         &mut self,
         surface_spatial_node_index: SpatialNodeIndex,
         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);
             }
         }
@@ -534,16 +763,39 @@ 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];
 
+                // Try to reuse cached tiles from the previous scene in this new
+                // scene, if possible.
+                if !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) {
+                        // Only use if not evicted from texture cache in the meantime.
+                        if !resource_cache.texture_cache.request(&handle, gpu_cache) {
+                            // We found a matching tile from the previous scene, so use it!
+                            tile.handle = 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,
                 );
 
@@ -560,54 +812,64 @@ impl TileCache {
                 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 node_index in &tile.transforms {
-                    if self.transforms[node_index.0].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, old) in &mut tile.opacity_bindings {
-                    if let Some(new) = frame_context.scene_properties.get_float_value(*id) {
-                        if !new.approx_eq(old) {
-                            tile.is_valid = false;
-                            break;
-                        }
+                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 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 we have an invalid tile, which is also visible, add it to the
                 // dirty rect we will need to draw.
-                if !tile.is_valid && tile.is_visible {
+                if !tile.is_valid && tile.is_visible && tile.in_use {
                     dirty_rect = dirty_rect.union(&tile_rect);
                     tile_offset.x = tile_offset.x.min(x);
                     tile_offset.y = tile_offset.y.min(y);
                 }
             }
         }
 
+        // If we had any retained tiles from the last scene that were not picked
+        // up by the new frame, then just discard them eagerly.
+        // TODO(gw): Maybe it's worth keeping them around for a bit longer in
+        //           some cases?
+        for (_, handle) in retained_tiles.drain() {
+            resource_cache.texture_cache.mark_unused(&handle);
+        }
+
         self.dirty_region = if dirty_rect.is_empty() {
             None
         } else {
             let dirty_world_rect = world_mapper.map(&dirty_rect).expect("todo");
             Some(DirtyRegion {
                 dirty_rect,
                 tile_offset,
                 dirty_world_rect,
@@ -1100,16 +1362,32 @@ impl PicturePrimitive {
         match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(ref filter)) => {
                 filter.is_visible()
             }
             _ => true,
         }
     }
 
+    /// 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 FastHashMap<TileDescriptor, TextureCacheHandle>,
+    ) {
+        if let Some(tile_cache) = self.tile_cache.take() {
+            debug_assert!(tile_cache.old_tiles.is_empty());
+            for tile in tile_cache.tiles {
+                retained_tiles.extend(tile.destroy());
+            }
+        }
+    }
+
     pub fn new_image(
         requested_composite_mode: Option<PictureCompositeMode>,
         context_3d: Picture3DContext<OrderedPictureChild>,
         pipeline_id: PipelineId,
         frame_output_pipeline_id: Option<PipelineId>,
         apply_local_clip_rect: bool,
         requested_raster_space: RasterSpace,
         prim_list: PrimitiveList,
@@ -1463,28 +1741,16 @@ impl PicturePrimitive {
         self.raster_config = None;
 
         // Resolve animation properties, and early out if the filter
         // properties make this picture invisible.
         if !self.resolve_scene_properties(frame_context.scene_properties) {
             return 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.
-        if let Some(mut tile_cache) = self.tile_cache.take() {
-            tile_cache.update_transforms(
-                self.spatial_node_index,
-                frame_context,
-            );
-
-            state.push_tile_cache(tile_cache);
-        }
-
         // Push information about this pic on stack for children to read.
         state.push_picture(PictureInfo {
             spatial_node_index: self.spatial_node_index,
         });
 
         // See if this picture actually needs a surface for compositing.
         let actual_composite_mode = match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(filter)) if filter.is_noop() => None,
@@ -1549,16 +1815,30 @@ impl PicturePrimitive {
                 )
             );
 
             self.raster_config = Some(RasterConfig {
                 composite_mode,
                 surface_index,
             });
 
+            // 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.
+            if let Some(mut tile_cache) = self.tile_cache.take() {
+                tile_cache.update_transforms(
+                    surface_spatial_node_index,
+                    raster_spatial_node_index,
+                    raster_space,
+                    frame_context,
+                );
+
+                state.push_tile_cache(tile_cache);
+            }
+
             // If we have a cache key / descriptor for this surface,
             // update any transforms it cares about.
             if let Some(ref mut surface_desc) = self.surface_desc {
                 surface_desc.update(
                     surface_spatial_node_index,
                     raster_spatial_node_index,
                     frame_context.clip_scroll_tree,
                     raster_space,
@@ -1581,42 +1861,45 @@ impl PicturePrimitive {
         clip_store: &ClipStore,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         if state.tile_cache_update_count == 0 {
             return;
         }
 
+        let surface_spatial_node_index = state.current_surface().surface_spatial_node_index;
+
         for prim_instance in &self.prim_list.prim_instances {
             for tile_cache in &mut state.tile_cache_stack {
                 tile_cache.update_prim_dependencies(
                     prim_instance,
-                    self.spatial_node_index,
+                    surface_spatial_node_index,
                     &frame_context.clip_scroll_tree,
                     prim_data_store,
                     &clip_store.clip_chain_nodes,
                     pictures,
                     resource_cache,
-                    frame_context.scene_properties,
                     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,
         resource_cache: &mut ResourceCache,
+        gpu_cache: &mut GpuCache,
+        retained_tiles: &mut FastHashMap<TileDescriptor, TextureCacheHandle>,
     ) {
         // Pop the state information about this picture.
         state.pop_picture();
 
         for cluster in &mut self.prim_list.clusters {
             // Skip the cluster if backface culled.
             if !cluster.is_backface_visible {
                 let containing_block_index = match self.context_3d {
@@ -1699,16 +1982,18 @@ impl PicturePrimitive {
             if let PictureCompositeMode::TileCache { .. } = raster_config.composite_mode {
                 let mut tile_cache = state.pop_tile_cache();
 
                 // Build the dirty region(s) for this tile cache.
                 tile_cache.build_dirty_regions(
                     self.spatial_node_index,
                     frame_context,
                     resource_cache,
+                    gpu_cache,
+                    retained_tiles,
                 );
 
                 self.tile_cache = Some(tile_cache);
             }
 
             let mut surface_rect = TypedRect::from_untyped(&surface_rect.to_untyped());
 
             // Pop this surface from the stack
@@ -1844,17 +2129,17 @@ impl PicturePrimitive {
                                         descriptor,
                                         TextureFilter::Linear,
                                         None,
                                         [0.0; 3],
                                         DirtyRect::All,
                                         frame_state.gpu_cache,
                                         None,
                                         UvRectKind::Rect,
-                                        Eviction::Eager,
+                                        Eviction::Auto,
                                     );
 
                                     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.
--- a/gfx/wr/webrender/src/prim_store.rs
+++ b/gfx/wr/webrender/src/prim_store.rs
@@ -10,39 +10,41 @@ use api::{PremultipliedColorF, PropertyB
 use api::{DeviceIntSideOffsets, WorldPixel, BoxShadowClipMode, NormalBorder, WorldRect, LayoutToWorldScale};
 use api::{PicturePixel, RasterPixel, ColorDepth, LineStyle, LineOrientation, LayoutSizeAu, AuHelpers, LayoutVector2DAu};
 use app_units::Au;
 use border::{get_max_scale_for_border, build_border_instances, create_border_segments};
 use border::{BorderSegmentCacheKey, NormalBorderAu};
 use clip::{ClipStore};
 use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
 use clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
-use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale};
+use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale, TypedSize2D};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveContext;
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks};
 use gpu_types::BrushFlags;
 use image::{self, Repetition};
 use intern;
+use internal_types::FastHashMap;
 use picture::{PictureCompositeMode, PicturePrimitive, PictureUpdateState};
-use picture::{ClusterRange, PrimitiveList, SurfaceIndex};
+use picture::{ClusterRange, PrimitiveList, SurfaceIndex, TileDescriptor};
 #[cfg(debug_assertions)]
 use render_backend::{FrameId};
 use render_backend::FrameResources;
 use render_task::{BlitSource, RenderTask, RenderTaskCacheKey, RenderTaskTree, to_cache_size};
 use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
 use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
 use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
 use scene::SceneProperties;
 use segment::SegmentBuilder;
 use std::{cmp, fmt, hash, mem, ops, u32, usize};
 #[cfg(debug_assertions)]
 use std::sync::atomic::{AtomicUsize, Ordering};
 use storage;
+use texture_cache::TextureCacheHandle;
 use tiling::SpecialRenderPasses;
 use util::{ScaleOffset, MatrixHelpers, MaxRect, recycle_vec};
 use util::{pack_as_float, project_rect, raster_rect_to_device_pixels};
 use smallvec::SmallVec;
 
 /// Counter for unique primitive IDs for debug tracing.
 #[cfg(debug_assertions)]
 static NEXT_PRIM_ID: AtomicUsize = AtomicUsize::new(0);
@@ -545,25 +547,34 @@ impl hash::Hash for SizeKey {
 }
 
 impl From<SizeKey> for LayoutSize {
     fn from(key: SizeKey) -> LayoutSize {
         LayoutSize::new(key.w, key.h)
     }
 }
 
-impl From<LayoutSize> for SizeKey {
-    fn from(size: LayoutSize) -> SizeKey {
+impl<U> From<TypedSize2D<f32, U>> for SizeKey {
+    fn from(size: TypedSize2D<f32, U>) -> SizeKey {
         SizeKey {
             w: size.width,
             h: size.height,
         }
     }
 }
 
+impl SizeKey {
+    pub fn zero() -> SizeKey {
+        SizeKey {
+            w: 0.0,
+            h: 0.0,
+        }
+    }
+}
+
 /// Hashable radial gradient parameters, for use during prim interning.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Clone, PartialEq)]
 pub struct RadialGradientParams {
     pub start_radius: f32,
     pub end_radius: f32,
     pub ratio_xy: f32,
@@ -2597,16 +2608,29 @@ impl PrimitiveStore {
         PrimitiveStoreStats {
             picture_count: self.pictures.len(),
             text_run_count: self.text_runs.len(),
             image_count: self.images.len(),
             opacity_binding_count: self.opacity_bindings.len(),
         }
     }
 
+    /// 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 FastHashMap<TileDescriptor, TextureCacheHandle>,
+    ) {
+        for pic in self.pictures {
+            pic.destroy(
+                retained_tiles,
+            );
+        }
+    }
+
     pub fn create_picture(
         &mut self,
         prim: PicturePrimitive,
     ) -> PictureIndex {
         let index = PictureIndex(self.pictures.len());
         self.prim_count += prim.prim_list.prim_instances.len();
         self.pictures.push(prim);
         index
@@ -2616,31 +2640,35 @@ impl PrimitiveStore {
     /// rasterization roots, and (in future) whether there
     /// are cached surfaces that can be used by this picture.
     pub fn update_picture(
         &mut self,
         pic_index: PictureIndex,
         state: &mut PictureUpdateState,
         frame_context: &FrameBuildingContext,
         resource_cache: &mut ResourceCache,
+        gpu_cache: &mut GpuCache,
         prim_data_store: &PrimitiveDataStore,
         clip_store: &ClipStore,
+        retained_tiles: &mut FastHashMap<TileDescriptor, TextureCacheHandle>,
     ) {
         if let Some(children) = self.pictures[pic_index.0].pre_update(
             state,
             frame_context,
         ) {
             for child_pic_index in &children {
                 self.update_picture(
                     *child_pic_index,
                     state,
                     frame_context,
                     resource_cache,
+                    gpu_cache,
                     prim_data_store,
                     clip_store,
+                    retained_tiles,
                 );
             }
 
             self.pictures[pic_index.0].update_prim_dependencies(
                 state,
                 frame_context,
                 resource_cache,
                 prim_data_store,
@@ -2650,16 +2678,18 @@ impl PrimitiveStore {
                 &self.images,
             );
 
             self.pictures[pic_index.0].post_update(
                 children,
                 state,
                 frame_context,
                 resource_cache,
+                gpu_cache,
+                retained_tiles,
             );
         }
     }
 
     pub fn get_opacity_binding(
         &self,
         opacity_binding_index: OpacityBindingIndex,
     ) -> f32 {
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -498,22 +498,40 @@ impl Document {
     ) -> bool {
         self.clip_scroll_tree.scroll_node(origin, id, clamp)
     }
 
     pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
         self.clip_scroll_tree.get_scroll_node_state()
     }
 
-    pub fn new_async_scene_ready(&mut self, built_scene: BuiltScene) {
+    pub fn new_async_scene_ready(
+        &mut self,
+        mut built_scene: BuiltScene,
+    ) {
         self.scene = built_scene.scene;
         self.frame_is_valid = false;
         self.hit_tester_is_valid = false;
 
+        // 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 = FastHashMap::default();
+        if let Some(frame_builder) = self.frame_builder.take() {
+            frame_builder.destroy(
+                &mut retained_tiles,
+            );
+        }
+
+        // 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);
+
         self.scratch.recycle();
 
         let old_scrolling_states = self.clip_scroll_tree.drain();
         self.clip_scroll_tree = built_scene.clip_scroll_tree;
         self.clip_scroll_tree.finalize_and_apply_pending_scroll_offsets(old_scrolling_states);
 
         // Advance to the next frame.
         self.stamp.advance();
@@ -736,17 +754,19 @@ impl RenderBackend {
                 match msg {
                     SceneBuilderResult::Transaction(mut txn, result_tx) => {
                         let has_built_scene = txn.built_scene.is_some();
                         if let Some(doc) = self.documents.get_mut(&txn.document_id) {
 
                             doc.removed_pipelines.append(&mut txn.removed_pipelines);
 
                             if let Some(mut built_scene) = txn.built_scene.take() {
-                                doc.new_async_scene_ready(built_scene);
+                                doc.new_async_scene_ready(
+                                    built_scene,
+                                );
                             }
 
                             if let Some(tx) = result_tx {
                                 let (resume_tx, resume_rx) = channel();
                                 tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
                                 // Block until the post-swap hook has completed on
                                 // the scene builder thread. We need to do this before
                                 // we can sample from the sampler hook which might happen
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.rs
@@ -108,23 +108,18 @@ impl SceneProperties {
                 self.float_properties
                     .get(&key.id)
                     .cloned()
                     .unwrap_or(v)
             }
         }
     }
 
-    pub fn get_float_value(
-        &self,
-        id: PropertyBindingId,
-    ) -> Option<f32> {
-        self.float_properties
-            .get(&id)
-            .cloned()
+    pub fn float_properties(&self) -> &FastHashMap<PropertyBindingId, f32> {
+        &self.float_properties
     }
 }
 
 /// A representation of the layout within the display port for a given document or iframe.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone)]
 pub struct ScenePipeline {
--- a/gfx/wr/webrender/src/surface.rs
+++ b/gfx/wr/webrender/src/surface.rs
@@ -53,20 +53,20 @@ fn quantize(value: f32) -> f32 {
 }
 
 /// A quantized, hashable version of util::ScaleOffset that
 /// can be used as a cache key.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, PartialEq, Clone)]
 pub struct ScaleOffsetKey {
-    scale_x: f32,
-    scale_y: f32,
-    offset_x: f32,
-    offset_y: f32,
+    pub scale_x: f32,
+    pub scale_y: f32,
+    pub offset_x: f32,
+    pub offset_y: f32,
 }
 
 impl ScaleOffsetKey {
     fn new(scale_offset: &ScaleOffset) -> Self {
         // TODO(gw): Since these are quantized, it might make sense in the future to
         //           convert these to ints to remove the need for custom hash impl.
         ScaleOffsetKey {
             scale_x: quantize(scale_offset.scale.x),