Bug 1516650 - Update webrender to commit b4dfe9c4f98fdeca3814976cd075bde8ed409123 (WR PR #3446). r=kats
authorWR Updater Bot <graphics-team@mozilla.staktrace.com>
Fri, 28 Dec 2018 19:27:55 +0000
changeset 452078 69cc7fba27dec865c30895f9e63e375afc7cf396
parent 452077 e16d8ebb698f5dd4fae9234d0ecb26edd58f3a12
child 452079 3f16eb2a8603abf8d9e28219671b394d5c9d2387
push id35283
push userdvarga@mozilla.com
push dateSat, 29 Dec 2018 09:34:06 +0000
treeherdermozilla-central@f2227be9432b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1516650
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1516650 - Update webrender to commit b4dfe9c4f98fdeca3814976cd075bde8ed409123 (WR PR #3446). r=kats https://github.com/servo/webrender/pull/3446 Differential Revision: https://phabricator.services.mozilla.com/D15480
gfx/webrender_bindings/revision.txt
gfx/wr/examples/animation.rs
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/clip.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/render_task.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/tiling.rs
gfx/wr/webrender/src/util.rs
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-7a9954180a978c2783257254c2c818c191276a91
+b4dfe9c4f98fdeca3814976cd075bde8ed409123
--- a/gfx/wr/examples/animation.rs
+++ b/gfx/wr/examples/animation.rs
@@ -36,20 +36,28 @@ struct App {
 
 impl App {
     fn add_rounded_rect(
         &mut self,
         bounds: LayoutRect,
         color: ColorF,
         builder: &mut DisplayListBuilder,
         property_key: PropertyBindingKey<LayoutTransform>,
+        opacity_key: Option<PropertyBindingKey<f32>>,
     ) {
-        let filters = [
-            FilterOp::Opacity(PropertyBinding::Binding(self.opacity_key, self.opacity), self.opacity),
-        ];
+        let filters = match opacity_key {
+            Some(opacity_key) => {
+                vec![
+                    FilterOp::Opacity(PropertyBinding::Binding(opacity_key, self.opacity), self.opacity),
+                ]
+            }
+            None => {
+                vec![]
+            }
+        };
 
         let reference_frame_id = builder.push_reference_frame(
             &LayoutRect::new(bounds.origin, LayoutSize::zero()),
             TransformStyle::Flat,
             Some(PropertyBinding::Binding(property_key, LayoutTransform::identity())),
             None,
         );
 
@@ -84,39 +92,41 @@ impl App {
         builder.pop_stacking_context();
 
         builder.pop_clip_id();
         builder.pop_reference_frame();
     }
 }
 
 impl Example for App {
-    const WIDTH: u32 = 1024;
-    const HEIGHT: u32 = 1024;
+    const WIDTH: u32 = 2048;
+    const HEIGHT: u32 = 1536;
 
     fn render(
         &mut self,
         _api: &RenderApi,
         builder: &mut DisplayListBuilder,
         _txn: &mut Transaction,
         _framebuffer_size: DeviceIntSize,
         _pipeline_id: PipelineId,
         _document_id: DocumentId,
     ) {
+        let opacity_key = self.opacity_key;
+
         let bounds = (150, 150).to(250, 250);
         let key0 = self.property_key0;
-        self.add_rounded_rect(bounds, ColorF::new(1.0, 0.0, 0.0, 0.5), builder, key0);
+        self.add_rounded_rect(bounds, ColorF::new(1.0, 0.0, 0.0, 0.5), builder, key0, Some(opacity_key));
 
         let bounds = (400, 400).to(600, 600);
         let key1 = self.property_key1;
-        self.add_rounded_rect(bounds, ColorF::new(0.0, 1.0, 0.0, 0.5), builder, key1);
+        self.add_rounded_rect(bounds, ColorF::new(0.0, 1.0, 0.0, 0.5), builder, key1, None);
 
         let bounds = (200, 500).to(350, 580);
         let key2 = self.property_key2;
-        self.add_rounded_rect(bounds, ColorF::new(0.0, 0.0, 1.0, 0.5), builder, key2);
+        self.add_rounded_rect(bounds, ColorF::new(0.0, 0.0, 1.0, 0.5), builder, key2, None);
     }
 
     fn on_event(&mut self, win_event: winit::WindowEvent, api: &RenderApi, document_id: DocumentId) -> bool {
         let mut rebuild_display_list = false;
 
         match win_event {
             winit::WindowEvent::KeyboardInput {
                 input: winit::KeyboardInput {
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -983,86 +983,105 @@ impl AlphaBatchBuilder {
                                 // brush primitive if visible.
 
                                 let kind = BatchKind::Brush(
                                     BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
                                 );
 
                                 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];
+                                // If there is a dirty rect for the tile cache, recurse into the
+                                // main picture primitive list, and draw them first.
+                                if let Some(_) = tile_cache.dirty_region {
+                                    self.add_pic_to_batch(
+                                        picture,
+                                        task_id,
+                                        ctx,
+                                        gpu_cache,
+                                        render_tasks,
+                                        deferred_resolves,
+                                        prim_headers,
+                                        transforms,
+                                        root_spatial_node_index,
+                                        z_generator,
+                                    );
+                                }
 
-                                        // Check if the tile is visible.
-                                        if !tile.is_visible || !tile.in_use {
-                                            continue;
-                                        }
-
-                                        // Get the local rect of the tile.
-                                        let tile_rect = tile_cache.get_tile_rect(x, y);
+                                // After drawing the dirty rect, now draw any of the valid tiles that
+                                // will make up the rest of the scene.
 
-                                        // Construct a local clip rect that ensures we only draw pixels where
-                                        // the local bounds of the picture extend to within the edge tiles.
-                                        let local_clip_rect = prim_instance
-                                            .combined_local_clip_rect
-                                            .intersection(&picture.local_rect)
-                                            .expect("bug: invalid picture local rect");
+                                // Generate a new z id for the tiles, that will place them *after*
+                                // any opaque overdraw from the dirty rect above.
+                                // TODO(gw): We should remove this hack, and also remove some
+                                //           (potential opaque) overdraw by adding support for
+                                //           setting a scissor rect for the dirty rect above.
+                                let tile_zid = z_generator.next();
+
+                                for tile_index in &tile_cache.tiles_to_draw {
+                                    let tile = &tile_cache.tiles[tile_index.0];
 
-                                        let prim_header = PrimitiveHeader {
-                                            local_rect: tile_rect,
-                                            local_clip_rect,
-                                            task_address,
-                                            specific_prim_address: prim_cache_address,
-                                            clip_task_address,
-                                            transform_id,
-                                        };
+                                    // Get the local rect of the tile.
+                                    let tile_rect = tile.local_rect;
 
-                                        let prim_header_index = prim_headers.push(&prim_header, z_id, [
-                                            ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
-                                            RasterizationSpace::Local as i32,
-                                            get_shader_opacity(1.0),
-                                        ]);
+                                    // Construct a local clip rect that ensures we only draw pixels where
+                                    // the local bounds of the picture extend to within the edge tiles.
+                                    let local_clip_rect = prim_instance
+                                        .combined_local_clip_rect
+                                        .intersection(&picture.local_rect)
+                                        .expect("bug: invalid picture local rect");
 
-                                        let cache_item = ctx
-                                            .resource_cache
-                                            .get_texture_cache_item(&tile.handle);
+                                    let prim_header = PrimitiveHeader {
+                                        local_rect: tile_rect,
+                                        local_clip_rect,
+                                        task_address,
+                                        specific_prim_address: prim_cache_address,
+                                        clip_task_address,
+                                        transform_id,
+                                    };
 
-                                        let key = BatchKey::new(
-                                            kind,
-                                            BlendMode::None,
-                                            BatchTextures::color(cache_item.texture_id),
-                                        );
+                                    let prim_header_index = prim_headers.push(&prim_header, tile_zid, [
+                                        ShaderColorMode::Image as i32 | ((AlphaType::PremultipliedAlpha as i32) << 16),
+                                        RasterizationSpace::Local as i32,
+                                        get_shader_opacity(1.0),
+                                    ]);
 
-                                        let uv_rect_address = gpu_cache
-                                            .get_address(&cache_item.uv_rect_handle)
-                                            .as_int();
+                                    let cache_item = ctx
+                                        .resource_cache
+                                        .get_texture_cache_item(&tile.handle);
+
+                                    let key = BatchKey::new(
+                                        kind,
+                                        BlendMode::None,
+                                        BatchTextures::color(cache_item.texture_id),
+                                    );
 
-                                        let instance = BrushInstance {
-                                            prim_header_index,
-                                            clip_task_address,
-                                            segment_index: INVALID_SEGMENT_INDEX,
-                                            edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
-                                            user_data: uv_rect_address,
-                                        };
+                                    let uv_rect_address = gpu_cache
+                                        .get_address(&cache_item.uv_rect_handle)
+                                        .as_int();
 
-                                        // Instead of retrieving the batch once and adding each tile instance,
-                                        // use this API to get an appropriate batch for each tile, since
-                                        // the batch textures may be different. The batch list internally
-                                        // caches the current batch if the key hasn't changed.
-                                        let batch = self.batch_list.set_params_and_get_batch(
-                                            key,
-                                            bounding_rect,
-                                            z_id,
-                                        );
+                                    let instance = BrushInstance {
+                                        prim_header_index,
+                                        clip_task_address,
+                                        segment_index: INVALID_SEGMENT_INDEX,
+                                        edge_flags: EdgeAaSegmentMask::empty(),
+                                        brush_flags: BrushFlags::empty(),
+                                        user_data: uv_rect_address,
+                                    };
 
-                                        batch.push(PrimitiveInstanceData::from(instance));
-                                    }
+                                    // Instead of retrieving the batch once and adding each tile instance,
+                                    // use this API to get an appropriate batch for each tile, since
+                                    // the batch textures may be different. The batch list internally
+                                    // caches the current batch if the key hasn't changed.
+                                    let batch = self.batch_list.set_params_and_get_batch(
+                                        key,
+                                        bounding_rect,
+                                        tile_zid,
+                                    );
+
+                                    batch.push(PrimitiveInstanceData::from(instance));
                                 }
                             }
                             PictureCompositeMode::Filter(filter) => {
                                 let surface = ctx.surfaces[raster_config.surface_index.0]
                                     .surface
                                     .as_ref()
                                     .expect("bug: surface must be allocated by now");
                                 assert!(filter.is_visible());
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -18,17 +18,17 @@ use image::{self, Repetition};
 use intern;
 use internal_types::FastHashSet;
 use prim_store::{ClipData, ImageMaskData, SpaceMapper, VisibleMaskImageTile};
 use prim_store::{PointKey, PrimitiveInstance, SizeKey, RectangleKey};
 use render_task::to_cache_size;
 use resource_cache::{ImageRequest, ResourceCache};
 use std::{cmp, u32};
 use std::os::raw::c_void;
-use util::{extract_inner_rect_safe, project_rect, ScaleOffset};
+use util::{extract_inner_rect_safe, project_rect, ScaleOffset, MaxRect};
 
 /*
 
  Module Overview
 
  There are a number of data structures involved in the clip module:
 
  ClipStore - Main interface used by other modules.
@@ -1356,16 +1356,56 @@ impl ClipNodeCollector {
     }
 
     pub fn insert(
         &mut self,
         clip_chain_id: ClipChainId,
     ) {
         self.clips.insert(clip_chain_id);
     }
+
+    pub fn clear(
+        &mut self,
+    ) {
+        self.clips.clear();
+    }
+
+    /// Build the world clip rect for this clip node collector.
+    // NOTE: This ignores any complex clips that may be present.
+    pub fn get_world_clip_rect(
+        &self,
+        clip_store: &ClipStore,
+        clip_data_store: &ClipDataStore,
+        clip_scroll_tree: &ClipScrollTree,
+    ) -> Option<WorldRect> {
+        let mut clip_rect = WorldRect::max_rect();
+
+        let mut map_local_to_world = SpaceMapper::new(
+            ROOT_SPATIAL_NODE_INDEX,
+            WorldRect::zero(),
+        );
+
+        for clip_chain_id in &self.clips {
+            let clip_chain_node = clip_store.get_clip_chain(*clip_chain_id);
+            let clip_node = &clip_data_store[clip_chain_node.handle];
+
+            if let Some(local_rect) = clip_node.item.get_local_clip_rect(clip_chain_node.local_pos) {
+                map_local_to_world.set_target_spatial_node(
+                    clip_chain_node.spatial_node_index,
+                    clip_scroll_tree,
+                );
+
+                if let Some(world_rect) = map_local_to_world.map(&local_rect) {
+                    clip_rect = clip_rect.intersection(&world_rect)?;
+                }
+            }
+        }
+
+        Some(clip_rect)
+    }
 }
 
 // Add a clip node into the list of clips to be processed
 // for the current clip chain. Returns false if the clip
 // results in the entire primitive being culled out.
 fn add_clip_node_to_current_chain(
     node: &ClipChainNode,
     spatial_node_index: SpatialNodeIndex,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -21,17 +21,17 @@ use profiler::{FrameProfileCounters, Gpu
 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, mem};
 use std::sync::Arc;
-use tiling::{Frame, RenderPass, RenderPassKind, RenderTargetContext};
+use tiling::{Frame, RenderPass, RenderPassKind, RenderTargetContext, RenderTarget};
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ChasePrimitive {
     Nothing,
     Id(PrimitiveDebugId),
@@ -286,24 +286,16 @@ impl FrameBuilder {
             resource_cache,
             resources,
             &self.clip_store,
             &pic_update_state.surfaces,
             gpu_cache,
             &mut retained_tiles,
         );
 
-        // 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.tiles.drain() {
-            resource_cache.texture_cache.mark_unused(&handle);
-        }
-
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
             transforms: transform_palette,
             segment_builder: SegmentBuilder::new(),
@@ -316,16 +308,17 @@ impl FrameBuilder {
             .take_context(
                 self.root_pic_index,
                 root_spatial_node_index,
                 root_spatial_node_index,
                 ROOT_SURFACE_INDEX,
                 true,
                 &mut frame_state,
                 &frame_context,
+                screen_world_rect,
             )
             .unwrap();
 
         self.prim_store.prepare_primitives(
             &mut prim_list,
             &pic_context,
             &mut pic_state,
             &frame_context,
@@ -341,26 +334,31 @@ impl FrameBuilder {
             pic_state,
             &mut frame_state,
         );
 
         let child_tasks = frame_state
             .surfaces[ROOT_SURFACE_INDEX.0]
             .take_render_tasks();
 
+        let tile_blits = mem::replace(
+            &mut frame_state.surfaces[ROOT_SURFACE_INDEX.0].tile_blits,
+            Vec::new(),
+        );
+
         let root_render_task = RenderTask::new_picture(
             RenderTaskLocation::Fixed(self.screen_rect.to_i32()),
             self.screen_rect.size.to_f32(),
             self.root_pic_index,
             DeviceIntPoint::zero(),
             child_tasks,
             UvRectKind::Rect,
             root_spatial_node_index,
             None,
-            Vec::new(),
+            tile_blits,
         );
 
         let render_task_id = frame_state.render_tasks.add(root_render_task);
         frame_state
             .surfaces
             .first_mut()
             .unwrap()
             .surface = Some(PictureSurface::RenderTask(render_task_id));
@@ -484,19 +482,24 @@ impl FrameBuilder {
                 &mut render_tasks,
                 &mut deferred_resolves,
                 &self.clip_store,
                 &mut transform_palette,
                 &mut prim_headers,
                 &mut z_generator,
             );
 
-            if let RenderPassKind::OffScreen { ref texture_cache, ref color, .. } = pass.kind {
-                has_texture_cache_tasks |= !texture_cache.is_empty();
-                has_texture_cache_tasks |= color.must_be_drawn();
+            match pass.kind {
+                RenderPassKind::MainFramebuffer(ref color) => {
+                    has_texture_cache_tasks |= color.must_be_drawn();
+                }
+                RenderPassKind::OffScreen { ref texture_cache, ref color, .. } => {
+                    has_texture_cache_tasks |= !texture_cache.is_empty();
+                    has_texture_cache_tasks |= color.must_be_drawn();
+                }
             }
         }
 
         let gpu_cache_frame_id = gpu_cache.end_frame(gpu_cache_profile);
 
         render_tasks.write_task_data(device_pixel_scale);
 
         resource_cache.end_frame();
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,45 +1,44 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-use api::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint};
+use api::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint, WorldPoint};
 use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
-use api::{DevicePixelScale, RasterRect, RasterSpace, DeviceIntPoint, ColorF, ImageKey, DirtyRect};
-use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, LayoutSize, LayoutPoint};
+use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize};
+use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, WorldVector2D};
 use box_shadow::{BLUR_SAMPLE_SCALE};
 use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode};
 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 intern::ItemUid;
 use internal_types::{FastHashMap, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 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::{get_raster_rects, CoordinateSpaceMapping, PointKey};
-use prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, SizeKey};
+use prim_store::{get_raster_rects, CoordinateSpaceMapping};
+use prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
 use print_tree::PrintTreePrinter;
 use render_backend::FrameResources;
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
 use scene_builder::DocumentResources;
 use smallvec::SmallVec;
 use surface::{SurfaceDescriptor, TransformKey};
 use std::{mem, u16};
 use texture_cache::{Eviction, TextureCacheHandle};
 use tiling::RenderTargetKind;
-use util::{TransformedRectKind, MatrixHelpers, MaxRect, RectHelpers};
+use util::{ComparableVec, TransformedRectKind, MatrixHelpers, MaxRect};
 
 /*
  A picture represents a dynamically rendered image. It consists of:
 
  * A number of primitives that are drawn onto the picture.
  * A composite operation describing how to composite this
    picture into its parent.
  * A configuration describing how to draw the primitives on
@@ -48,58 +47,45 @@ use util::{TransformedRectKind, MatrixHe
 
 /// Information about a picture that is pushed / popped on the
 /// PictureUpdateState during picture traversal pass.
 struct PictureInfo {
     /// The spatial node for this picture.
     spatial_node_index: SpatialNodeIndex,
 }
 
-/// Stores a map of cached picture tiles that are retained
+/// Stores a list of cached picture tiles that are retained
 /// between new scenes.
 pub struct RetainedTiles {
-    pub tiles: FastHashMap<TileDescriptor, TextureCacheHandle>,
+    pub tiles: Vec<Tile>,
 }
 
 impl RetainedTiles {
     pub fn new() -> Self {
         RetainedTiles {
-            tiles: FastHashMap::default(),
+            tiles: Vec::new(),
         }
     }
 }
 
 /// Unit for tile coordinates.
 #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub struct TileCoordinate;
 
 // Geometry types for tile coordinates.
 pub type TileOffset = TypedPoint2D<i32, TileCoordinate>;
 pub type TileSize = TypedSize2D<i32, TileCoordinate>;
-pub type TileRect = TypedRect<i32, TileCoordinate>;
+pub struct TileIndex(pub usize);
 
 /// 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_WIDTH: i32 = 1024;
 pub const TILE_SIZE_HEIGHT: i32 = 256;
 
-/// The maximum size of a picture before we disable tile caching.
-const MAX_PICTURE_SIZE: f32 = 65536.0;
-
-/// Information about the state of a transform dependency.
-#[derive(Debug)]
-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.
     /// Only calculated on first use.
     current: Option<TransformKey>,
     /// Tiles check this to see if the dependencies have changed.
     changed: bool,
@@ -112,369 +98,263 @@ pub struct OpacityBindingInfo {
     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 {
-    /// The set of opacity bindings that this tile depends on.
-    opacity_bindings: FastHashSet<PropertyBindingId>,
-    /// Set of image keys that this tile depends on.
-    image_keys: FastHashSet<ImageKey>,
+    /// The current world rect of thie tile.
+    world_rect: WorldRect,
+    /// The current local rect of this tile.
+    pub local_rect: LayoutRect,
+    /// The valid rect within this tile.
+    pixel_rect: Option<DeviceIntRect>,
+    /// Uniquely describes the content of this tile, in a way that can be
+    /// (reasonably) efficiently hashed and compared.
+    descriptor: TileDescriptor,
+    /// Handle to the cached texture for this tile.
+    pub handle: TextureCacheHandle,
     /// If true, this tile is marked valid, and the existing texture
     /// cache handle can be used. Tiles are invalidated during the
     /// build_dirty_regions method.
     is_valid: bool,
-    /// 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.
-    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(
-        tile_offset: TileOffset,
-        local_tile_size: SizeKey,
-        raster_transform: TransformKey,
     ) -> Self {
         Tile {
-            opacity_bindings: FastHashSet::default(),
-            image_keys: FastHashSet::default(),
+            handle: TextureCacheHandle::invalid(),
+            local_rect: LayoutRect::zero(),
+            world_rect: WorldRect::zero(),
+            descriptor: TileDescriptor::new(),
             is_valid: false,
-            is_visible: false,
-            is_cacheable: true,
-            in_use: false,
-            handle: TextureCacheHandle::invalid(),
-            descriptor: TileDescriptor::new(
-                tile_offset,
-                local_tile_size,
-                raster_transform,
-            ),
-            tile_transform_map: FastHashMap::default(),
-            transform_info: Vec::new(),
+            pixel_rect: None,
         }
     }
 
-    /// 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: &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)
-            .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");
-
-                // 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,
-                    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
-        }
+    /// Clear the dependencies for a tile.
+    fn clear(&mut self) {
+        self.descriptor.clear();
     }
 }
 
-/// Index of a transform array local to the tile.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct TileTransformIndex(u32);
-
 /// Defines a key that uniquely identifies a primitive instance.
-#[derive(Debug, Eq, PartialEq, Hash)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct PrimitiveDescriptor {
     /// Uniquely identifies the content of the primitive template.
     prim_uid: ItemUid,
-    /// The origin in local space of this primitive.
-    origin: PointKey,
+    /// The origin in world space of this primitive.
+    origin: WorldPoint,
     /// The first clip in the clip_uids array of clips that affect this tile.
     first_clip: u16,
     /// The number of clips that affect this primitive instance.
     clip_count: u16,
 }
 
 /// Uniquely describes the content of this tile, in a way that can be
 /// (reasonably) efficiently hashed and compared.
-#[derive(Debug, Eq, PartialEq, Hash)]
+#[derive(Debug)]
 pub struct TileDescriptor {
     /// List of primitive instance unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the primitive template, while
     /// the other parameters describe the clip chain and instance params.
-    pub prims: Vec<PrimitiveDescriptor>,
+    prims: ComparableVec<PrimitiveDescriptor>,
 
     /// List of clip node unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the clip node.
-    pub clip_uids: Vec<ItemUid>,
+    clip_uids: ComparableVec<ItemUid>,
 
-    /// 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>,
+    /// List of image keys that this tile depends on.
+    image_keys: ComparableVec<ImageKey>,
 
     /// The set of opacity bindings that this tile depends on.
     // TODO(gw): Ugh, get rid of all opacity binding support!
-    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,
+    opacity_bindings: ComparableVec<PropertyBindingId>,
 }
 
 impl TileDescriptor {
-    fn new(
-        tile_offset: TileOffset,
-        local_tile_size: SizeKey,
-        raster_transform: TransformKey,
-    ) -> Self {
+    fn new() -> Self {
         TileDescriptor {
-            prims: Vec::new(),
-            clip_uids: Vec::new(),
-            transform_ids: Vec::new(),
-            opacity_bindings: Vec::new(),
-            transforms: Vec::new(),
-            tile_offset,
-            raster_transform,
-            local_tile_size,
+            prims: ComparableVec::new(),
+            clip_uids: ComparableVec::new(),
+            opacity_bindings: ComparableVec::new(),
+            image_keys: ComparableVec::new(),
         }
     }
 
     /// Clear the dependency information for a tile, when the dependencies
     /// are being rebuilt.
     fn clear(&mut self) {
-        self.prims.clear();
-        self.clip_uids.clear();
-        self.transform_ids.clear();
-        self.transforms.clear();
-        self.opacity_bindings.clear();
+        self.prims.reset();
+        self.clip_uids.reset();
+        self.opacity_bindings.reset();
+        self.image_keys.reset();
+    }
+
+    /// Check if the dependencies of this tile are valid.
+    fn is_valid(&self) -> bool {
+        self.image_keys.is_valid() &&
+        self.opacity_bindings.is_valid() &&
+        self.clip_uids.is_valid() &&
+        self.prims.is_valid()
     }
 }
 
 /// 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,
-    dirty_rect: LayoutRect,
     dirty_world_rect: WorldRect,
 }
 
 /// Represents a cache of tiles that make up a picture primitives.
 pub struct TileCache {
-    /// The size of each tile in local-space coordinates of the picture.
-    pub local_tile_size: LayoutSize,
+    /// The positioning node for this tile cache.
+    spatial_node_index: SpatialNodeIndex,
     /// List of tiles present in this picture (stored as a 2D array)
     pub tiles: Vec<Tile>,
-    /// A 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,
+    /// A helper struct to map local rects into world coords.
+    map_local_to_world: SpaceMapper<LayoutPixel, WorldPixel>,
+    /// A list of tiles to draw during batching.
+    pub tiles_to_draw: Vec<TileIndex>,
     /// List of transform keys - used to check if transforms
     /// have changed.
-    pub transforms: Vec<GlobalTransformInfo>,
+    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, LayoutPixel>,
+    opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+    /// If Some(..) the region that is dirty in this picture.
+    pub dirty_region: Option<DirtyRegion>,
     /// If true, we need to update the prim dependencies, due
     /// to relative transforms changing. The dependencies are
     /// stored in each tile, and are a list of things that
     /// 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,
-
-    /// Contains the offset between the local picture rect that this
-    /// tile cache covers, and the aligned origin where tiles are
-    /// placed. This ensures that tiles are placed on correctly
-    /// aligned locations between new scenes when the enclosing
-    /// picture rect has a different local origin.
-    local_origin: LayoutPoint,
+    needs_update: bool,
+    /// The current world reference point that tiles are created around.
+    world_origin: WorldPoint,
+    /// Current size of tiles in world units.
+    world_tile_size: WorldSize,
+    /// Current number of tiles in the allocated grid.
+    tile_count: TileSize,
+    /// The current scroll offset for this frame builder. Reset when
+    /// a new scene arrives.
+    scroll_offset: Option<WorldVector2D>,
+    /// A list of blits from the framebuffer to be applied during this frame.
+    pending_blits: Vec<TileBlit>,
+    /// Collects the clips that apply to this surface.
+    clip_node_collector: ClipNodeCollector,
 }
 
 impl TileCache {
-    /// Construct a new tile cache.
-    pub fn new() -> Self {
+    pub fn new(spatial_node_index: SpatialNodeIndex) -> Self {
         TileCache {
+            spatial_node_index,
             tiles: Vec::new(),
-            old_tiles: FastHashMap::default(),
-            tile_rect: TileRect::zero(),
-            local_tile_size: LayoutSize::zero(),
+            map_local_to_world: SpaceMapper::new(
+                ROOT_SPATIAL_NODE_INDEX,
+                WorldRect::zero(),
+            ),
+            tiles_to_draw: Vec::new(),
             transforms: Vec::new(),
             opacity_bindings: FastHashMap::default(),
+            dirty_region: None,
             needs_update: true,
-            dirty_region: None,
-            space_mapper: SpaceMapper::new(
-                ROOT_SPATIAL_NODE_INDEX,
-                LayoutRect::zero(),
-            ),
-            raster_transform: TransformKey::Local,
-            local_origin: LayoutPoint::zero(),
+            world_origin: WorldPoint::zero(),
+            world_tile_size: WorldSize::zero(),
+            tile_count: TileSize::zero(),
+            scroll_offset: None,
+            pending_blits: Vec::new(),
+            clip_node_collector: ClipNodeCollector::new(spatial_node_index),
         }
     }
 
-    /// 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(
+    /// Get the tile coordinates for a given rectangle.
+    fn get_tile_coords_for_rect(
+        &self,
+        rect: &WorldRect,
+    ) -> (TileOffset, TileOffset) {
+        // Translate the rectangle into the virtual tile space
+        let origin = rect.origin - self.world_origin;
+
+        // Get the tile coordinates in the picture space.
+        let p0 = TileOffset::new(
+            (origin.x / self.world_tile_size.width).floor() as i32,
+            (origin.y / self.world_tile_size.height).floor() as i32,
+        );
+
+        let p1 = TileOffset::new(
+            ((origin.x + rect.size.width) / self.world_tile_size.width).ceil() as i32,
+            ((origin.y + rect.size.height) / self.world_tile_size.height).ceil() as i32,
+        );
+
+        (p0, p1)
+    }
+
+    /// Update transforms, opacity bindings and tile rects.
+    pub fn pre_update(
         &mut self,
-        surface_spatial_node_index: SpatialNodeIndex,
-        raster_spatial_node_index: SpatialNodeIndex,
-        raster_space: RasterSpace,
+        pic_rect: LayoutRect,
         frame_context: &FrameBuildingContext,
-        pic_rect: LayoutRect,
+        resource_cache: &ResourceCache,
+        retained_tiles: &mut RetainedTiles,
     ) {
-        // If we previously disabled the tile cache due to an invalid
-        // picture rect, and then we re-enable it, force an update
-        // of all primitive dependencies.
-        if self.tile_rect.size.is_empty_or_negative() {
-            self.needs_update = true;
+        // Work out the scroll offset to apply to the world reference point.
+        let scroll_transform = frame_context.clip_scroll_tree.get_relative_transform(
+            ROOT_SPATIAL_NODE_INDEX,
+            self.spatial_node_index,
+        ).expect("bug: unable to get scroll transform");
+        let scroll_offset = WorldVector2D::new(
+            scroll_transform.m41,
+            scroll_transform.m42,
+        );
+        let scroll_delta = match self.scroll_offset {
+            Some(prev) => prev - scroll_offset,
+            None => WorldVector2D::zero(),
+        };
+        self.scroll_offset = Some(scroll_offset);
+
+        // Pull any retained tiles from the previous scene.
+        if !retained_tiles.tiles.is_empty() {
+            assert!(self.tiles.is_empty());
+            self.tiles = mem::replace(&mut retained_tiles.tiles, Vec::new());
         }
 
-        // Initialize the space mapper with current bounds,
-        // which is used during primitive dependency updates.
+        // Assume no tiles are valid to draw by default
+        self.tiles_to_draw.clear();
+
+        self.map_local_to_world = SpaceMapper::new(
+            ROOT_SPATIAL_NODE_INDEX,
+            frame_context.screen_world_rect,
+        );
+
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
-            surface_spatial_node_index,
+            self.spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
-        let pic_bounds = world_mapper
-            .unmap(&frame_context.screen_world_rect)
-            .unwrap_or(LayoutRect::max_rect());
-
-        self.space_mapper = SpaceMapper::new(
-            surface_spatial_node_index,
-            pic_bounds,
-        );
-
-        // Work out the local space size of each tile, based on the
-        // device pixel size of tiles.
-        // TODO(gw): Perhaps add a map_point API to SpaceMapper.
-        let world_tile_rect = WorldRect::from_floats(
-            0.0,
-            0.0,
-            TILE_SIZE_WIDTH as f32 / frame_context.device_pixel_scale.0,
-            TILE_SIZE_HEIGHT as f32 / frame_context.device_pixel_scale.0,
-        );
-        let local_tile_rect = world_mapper
-            .unmap(&world_tile_rect)
-            .expect("bug: unable to get local tile size");
-        self.local_tile_size = local_tile_rect.size;
-
-        // Round the local reference point down to a whole number. This ensures
-        // that the bounding rect of the tile corresponds to a pixel boundary, and
-        // the content is offset by a fractional amount inside the surface itself.
-        // This means that when drawing the tile it's fine to use a simple 0-1
-        // UV mapping, instead of trying to determine a fractional UV rect that
-        // is slightly inside the allocated tile surface.
-        self.local_origin = pic_rect.origin.floor();
-
         // Walk the transforms and see if we need to rebuild the primitive
         // dependencies for each tile.
         // TODO(gw): We could be smarter here and only rebuild for the primitives
         //           which are affected by transforms that have changed.
         if self.transforms.len() == frame_context.clip_scroll_tree.spatial_nodes.len() {
             for (i, transform) in self.transforms.iter_mut().enumerate() {
                 // If this relative transform was used on the previous frame,
                 // update it and store whether it changed for use during
                 // tile invalidation later.
                 if let Some(ref mut current) = transform.current {
                     let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
-                        surface_spatial_node_index,
+                        self.spatial_node_index,
                         SpatialNodeIndex(i),
                         frame_context.clip_scroll_tree,
                     ).expect("todo: handle invalid mappings");
 
                     let key = mapping.into();
                     transform.changed = key != *current;
                     *current = key;
                 }
@@ -502,203 +382,171 @@ impl TileCache {
                 None => true,
             };
             self.opacity_bindings.insert(*id, OpacityBindingInfo {
                 value: *value,
                 changed,
             });
         }
 
-        // Update the state of the transform for compositing this picture.
-        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.
+        // Map the picture rect to world space and work out the tiles that we need
+        // in order to ensure the screen is covered.
+        let pic_world_rect = world_mapper
+            .map(&pic_rect)
+            .expect("bug: unable to map picture rect to world");
 
-                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();
+        // TODO(gw): Inflate the world rect a bit, to ensure that we keep tiles in
+        //           the cache for a while after they disappear off-screen.
+        let needed_world_rect = frame_context
+            .screen_world_rect
+            .intersection(&pic_world_rect);
 
-                if let TransformKey::ScaleOffset(ref mut key) = key {
-                    key.offset_x = 0.0;
-                    key.offset_y = 0.0;
-                }
-
-                key
-            }
-            RasterSpace::Local(..) => {
-                TransformKey::local()
+        let needed_world_rect = match needed_world_rect {
+            Some(rect) => rect,
+            None => {
+                // TODO(gw): Should we explicitly drop any existing cache handles here?
+                self.tiles.clear();
+                return;
             }
         };
 
-        // 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 = self.raster_transform.clone();
+        // Get a reference point that serves as an origin that all tiles we create
+        // must be aligned to. This ensures that tiles get reused correctly between
+        // scrolls and display list changes, even with the different local coord
+        // systems that gecko supplies.
+        let mut world_ref_point = if self.tiles.is_empty() {
+            needed_world_rect.origin.floor()
+        } else {
+            self.tiles[0].world_rect.origin
+        };
+
+        // Apply the scroll delta so that existing tiles still get used.
+        world_ref_point += scroll_delta;
+
+        // Work out the required device rect that we need to cover the screen,
+        // given the world reference point constraint.
+        let device_ref_point = world_ref_point * frame_context.device_pixel_scale;
+        let device_world_rect = frame_context.screen_world_rect * frame_context.device_pixel_scale;
+        let pic_device_rect = pic_world_rect * frame_context.device_pixel_scale;
+        let needed_device_rect = pic_device_rect
+            .intersection(&device_world_rect)
+            .expect("todo: handle clipped device rect");
+
+        let p0 = needed_device_rect.origin;
+        let p1 = needed_device_rect.bottom_right();
+
+        let p0 = DevicePoint::new(
+            device_ref_point.x + ((p0.x - device_ref_point.x) / TILE_SIZE_WIDTH as f32).floor() * TILE_SIZE_WIDTH as f32,
+            device_ref_point.y + ((p0.y - device_ref_point.y) / TILE_SIZE_HEIGHT as f32).floor() * TILE_SIZE_HEIGHT as f32,
+        );
+
+        let p1 = DevicePoint::new(
+            device_ref_point.x + ((p1.x - device_ref_point.x) / TILE_SIZE_WIDTH as f32).ceil() * TILE_SIZE_WIDTH as f32,
+            device_ref_point.y + ((p1.y - device_ref_point.y) / TILE_SIZE_HEIGHT as f32).ceil() * TILE_SIZE_HEIGHT as f32,
+        );
+
+        // And now the number of tiles from that device rect.
+        let x_tiles = ((p1.x - p0.x) / TILE_SIZE_WIDTH as f32).round() as i32;
+        let y_tiles = ((p1.y - p0.y) / TILE_SIZE_HEIGHT as f32).round() as i32;
 
-            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();
+        // Step through any old tiles, and retain them if we can. They are keyed only on
+        // the (scroll adjusted) world position, relying on the descriptor content checks
+        // later to invalidate them if the content has changed.
+        let mut old_tiles = FastHashMap::default();
+        for tile in self.tiles.drain(..) {
+            let tile_device_pos = (tile.world_rect.origin + scroll_delta) * frame_context.device_pixel_scale;
+            let key = (tile_device_pos.x.round() as i32, tile_device_pos.y.round() as i32);
+            old_tiles.insert(key, tile);
+        }
+
+        // Store parameters about the current tiling rect for use during dependency updates.
+        self.world_origin = WorldPoint::new(
+            p0.x / frame_context.device_pixel_scale.0,
+            p0.y / frame_context.device_pixel_scale.0,
+        );
+        self.world_tile_size = WorldSize::new(
+            TILE_SIZE_WIDTH as f32 / frame_context.device_pixel_scale.0,
+            TILE_SIZE_HEIGHT as f32 / frame_context.device_pixel_scale.0,
+        );
+        self.tile_count = TileSize::new(x_tiles, y_tiles);
 
-                info.changed = *transform != new_transform;
-                *transform = new_transform;
+        // Step through each tile and try to retain an old tile from the
+        // previous frame, and update bounding rects.
+        for y in 0 .. y_tiles {
+            for x in 0 .. x_tiles {
+                let px = p0.x + x as f32 * TILE_SIZE_WIDTH as f32;
+                let py = p0.y + y as f32 * TILE_SIZE_HEIGHT as f32;
+                let key = (px.round() as i32, py.round() as i32);
+
+                let mut tile = match old_tiles.remove(&key) {
+                    Some(tile) => tile,
+                    None => Tile::new(),
+                };
 
-                self.needs_update |= info.changed;
+                tile.world_rect = WorldRect::new(
+                    WorldPoint::new(
+                        px / frame_context.device_pixel_scale.0,
+                        py / frame_context.device_pixel_scale.0,
+                    ),
+                    self.world_tile_size,
+                );
+
+                tile.local_rect = world_mapper
+                    .unmap(&tile.world_rect)
+                    .expect("bug: can't unmap world rect");
+
+                self.tiles.push(tile);
             }
         }
 
-        // 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.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);
-            }
-
-            // Reset the size of the tile grid.
-            self.tile_rect = TileRect::zero();
+        if !old_tiles.is_empty() {
+            // TODO(gw): Should we explicitly drop the tile texture cache handles here?
         }
 
-        // Get the tile coordinates in the picture space.
-        let pic_rect = TypedRect::from_untyped(&pic_rect.to_untyped());
-        let (p0, p1) = self.get_tile_coords_for_rect(&pic_rect);
-
-        // Update the tile array allocation if needed.
-        self.reconfigure_tiles_if_required(p0.x, p0.y, p1.x, p1.y);
-    }
+        // TODO(gw): We don't actually need to update the prim dependencies each frame.
+        //           For common cases, such as only being one main scroll root, we could
+        //           detect this and skip the dependency update on scroll frames.
+        self.needs_update = true;
+        self.clip_node_collector.clear();
 
-    /// Get the tile coordinates for a given rectangle.
-    fn get_tile_coords_for_rect(
-        &self,
-        rect: &LayoutRect,
-    ) -> (TileOffset, TileOffset) {
-        // Translate the rectangle into the virtual tile space
-        let origin = rect.origin - self.local_origin;
-
-        // Get the tile coordinates in the picture space.
-        let p0 = TileOffset::new(
-            (origin.x / self.local_tile_size.width).floor() as i32,
-            (origin.y / self.local_tile_size.height).floor() as i32,
-        );
-
-        let p1 = TileOffset::new(
-            ((origin.x + rect.size.width) / self.local_tile_size.width).ceil() as i32,
-            ((origin.y + rect.size.height) / self.local_tile_size.height).ceil() as i32,
-        );
-
-        (p0, p1)
-    }
+        // Do tile invalidation for any dependencies that we know now.
+        for tile in &mut self.tiles {
+            // Invalidate the tile if any images have changed
+            for image_key in tile.descriptor.image_keys.items() {
+                if resource_cache.is_image_dirty(*image_key) {
+                    tile.is_valid = false;
+                    break;
+                }
+            }
 
-    /// Resize the 2D tiles array if needed in order to fit dependencies
-    /// for a given primitive.
-    fn reconfigure_tiles_if_required(
-        &mut self,
-        mut x0: i32,
-        mut y0: i32,
-        mut x1: i32,
-        mut y1: i32,
-    ) {
-        // Determine and store the tile bounds that are now required.
-        if self.tile_rect.size.width > 0 {
-            x0 = x0.min(self.tile_rect.origin.x);
-            x1 = x1.max(self.tile_rect.origin.x + self.tile_rect.size.width);
-        }
-        if self.tile_rect.size.height > 0 {
-            y0 = y0.min(self.tile_rect.origin.y);
-            y1 = y1.max(self.tile_rect.origin.y + self.tile_rect.size.height);
-        }
-
-        // See how many tiles are now required, and if that's different from current config.
-        let x_tiles = x1 - x0;
-        let y_tiles = y1 - y0;
-
-        // Early exit if the tile configuration is the same.
-        if self.tile_rect.size.width == x_tiles &&
-           self.tile_rect.size.height == y_tiles &&
-           self.tile_rect.origin.x == x0 &&
-           self.tile_rect.origin.y == y0 {
-            return;
-        }
+            // Invalidate the tile if any opacity bindings changed.
+            for id in tile.descriptor.opacity_bindings.items() {
+                let changed = match self.opacity_bindings.get(id) {
+                    Some(info) => info.changed,
+                    None => true,
+                };
+                if changed {
+                    tile.is_valid = false;
+                    break;
+                }
+            }
 
-        // We will need to create a new tile array.
-        let mut new_tiles = Vec::with_capacity((x_tiles * y_tiles) as usize);
-
-        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(
-                            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,
-                            self.local_tile_size.into(),
-                            self.raster_transform.clone(),
-                        )
-                    })
-                };
-                new_tiles.push(tile);
+            if self.needs_update {
+                // Clear any dependencies so that when we rebuild them we
+                // can compare if the tile has the same content.
+                tile.clear();
             }
         }
-
-        self.tiles = new_tiles;
-        self.tile_rect.origin = TileOffset::new(x0, y0);
-        self.tile_rect.size = TileSize::new(x_tiles, y_tiles);
     }
 
     /// Update the dependencies for each tile for a given primitive instance.
     pub fn update_prim_dependencies(
         &mut self,
         prim_instance: &PrimitiveInstance,
         prim_list: &PrimitiveList,
-        surface_spatial_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
         resources: &FrameResources,
         clip_chain_nodes: &[ClipChainNode],
         pictures: &[PicturePrimitive],
         resource_cache: &ResourceCache,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
@@ -709,17 +557,17 @@ impl TileCache {
         // We need to ensure that if a primitive belongs to a cluster that has
         // been marked invisible, we exclude it here. Otherwise, we may end up
         // with a primitive that is outside the bounding rect of the calculated
         // picture rect (which takes the cluster visibility into account).
         if !prim_list.clusters[prim_instance.cluster_index.0 as usize].is_visible {
             return;
         }
 
-        self.space_mapper.set_target_spatial_node(
+        self.map_local_to_world.set_target_spatial_node(
             prim_instance.spatial_node_index,
             clip_scroll_tree,
         );
 
         let prim_data = &resources.as_common_data(&prim_instance);
 
         let (prim_rect, clip_rect) = match prim_instance.kind {
             PrimitiveInstanceKind::Picture { pic_index, .. } => {
@@ -743,31 +591,31 @@ impl TileCache {
         // TODO(gw): We should maybe store this in the primitive template
         //           during interning so that we never have to calculate
         //           it during frame building.
         let culling_rect = match prim_rect.intersection(&clip_rect) {
             Some(rect) => rect,
             None => return,
         };
 
-        let rect = match self.space_mapper.map(&culling_rect) {
+        let world_rect = match self.map_local_to_world.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.
-        if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
+        if world_rect.size.width <= 0.0 || world_rect.size.height <= 0.0 {
             return;
         }
 
         // Get the tile coordinates in the picture space.
-        let (p0, p1) = self.get_tile_coords_for_rect(&rect);
+        let (p0, p1) = self.get_tile_coords_for_rect(&world_rect);
 
         // Build the list of resources that this primitive has dependencies on.
         let mut opacity_bindings: SmallVec<[PropertyBindingId; 4]> = SmallVec::new();
         let mut clip_chain_spatial_nodes: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
         let mut clip_chain_uids: SmallVec<[ItemUid; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
         let mut current_clip_chain_id = prim_instance.clip_chain_id;
 
@@ -830,244 +678,215 @@ impl TileCache {
 
         // 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 {
+            if clip_chain_node.spatial_node_index < self.spatial_node_index {
+                self.clip_node_collector.insert(current_clip_chain_id)
+            } else {
                 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 p0.y - self.tile_rect.origin.y .. p1.y - self.tile_rect.origin.y {
-            for x in p0.x - self.tile_rect.origin.x .. p1.x - self.tile_rect.origin.x {
-                let index = (y * self.tile_rect.size.width + x) as usize;
+        for y in p0.y .. p1.y {
+            for x in p0.x .. p1.x {
+                // If the primitive exists on tiles outside the selected tile cache
+                // area, just ignore those.
+                if x < 0 || x >= self.tile_count.width || y < 0 || y >= self.tile_count.height {
+                    continue;
+                }
+
+                let index = (y * self.tile_count.width + x) as usize;
                 let tile = &mut self.tiles[index];
 
                 // Mark if the tile is cacheable at all.
-                tile.is_cacheable &= is_cacheable;
-                tile.in_use = true;
+                tile.is_valid &= is_cacheable;
 
                 // 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.push_transform_dependency(
-                    prim_instance.spatial_node_index,
-                    surface_spatial_node_index,
-                    clip_scroll_tree,
-                    &mut self.transforms,
-                );
+                tile.descriptor.image_keys.extend_from_slice(&image_keys);
 
-                // 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,
-                        &mut self.transforms,
-                    );
-                }
+                // // Include any opacity bindings this primitive depends on.
+                tile.descriptor.opacity_bindings.extend_from_slice(&opacity_bindings);
 
-                // 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);
-                    }
-                }
-
-                // For the primitive origin, store the local origin relative to
-                // the local origin of the containing picture. This ensures that
+                // For the primitive origin, store the world origin relative to
+                // the world origin of the containing picture. This ensures that
                 // a tile with primitives in the same coordinate system as the
                 // container picture itself, but different offsets relative to
                 // the containing picture are correctly invalidated. It does this
                 // while still maintaining the property of keeping the same hash
                 // for different display lists where the local origin is different
                 // but the primitives themselves are at the same relative position.
-                let origin = PointKey {
-                    x: prim_rect.origin.x - self.local_origin.x,
-                    y: prim_rect.origin.y - self.local_origin.y,
-                };
+                let origin = WorldPoint::new(
+                    world_rect.origin.x - tile.world_rect.origin.x,
+                    world_rect.origin.y - tile.world_rect.origin.y
+                );
 
                 // Update the tile descriptor, used for tile comparison during scene swaps.
                 tile.descriptor.prims.push(PrimitiveDescriptor {
                     prim_uid: prim_instance.uid(),
                     origin,
                     first_clip: tile.descriptor.clip_uids.len() as u16,
                     clip_count: clip_chain_uids.len() as u16,
                 });
                 tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
             }
         }
     }
 
-    /// Get a local space rectangle for a given tile coordinate.
-    pub fn get_tile_rect(&self, x: i32, y: i32) -> LayoutRect {
-        LayoutRect::new(
-            LayoutPoint::new(
-                self.local_origin.x + (self.tile_rect.origin.x + x) as f32 * self.local_tile_size.width,
-                self.local_origin.y + (self.tile_rect.origin.y + y) as f32 * self.local_tile_size.height,
-            ),
-            self.local_tile_size,
-        )
-    }
-
-    /// Build the dirty region(s) for the tile cache after all primitive
-    /// dependencies have been updated.
-    pub fn build_dirty_regions(
+    /// Apply any updates after prim dependency updates. This applies
+    /// any late tile invalidations, and sets up the dirty rect and
+    /// set of tile blits.
+    pub fn post_update(
         &mut self,
-        surface_spatial_node_index: SpatialNodeIndex,
-        frame_context: &FrameBuildingContext,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
-        retained_tiles: &mut RetainedTiles,
+        clip_store: &ClipStore,
+        frame_context: &FrameBuildingContext,
+        resources: &FrameResources,
     ) {
-        self.needs_update = false;
+        let mut dirty_world_rect = WorldRect::zero();
 
-        for (_, tile) in self.old_tiles.drain() {
-            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,
+        let descriptor = ImageDescriptor::new(
+            TILE_SIZE_WIDTH,
+            TILE_SIZE_HEIGHT,
+            ImageFormat::BGRA8,
+            true,
+            false,
         );
 
-        let mut tile_offset = DeviceIntPoint::new(
-            self.tile_rect.size.width,
-            self.tile_rect.size.height,
-        );
+        // Get the world clip rect that we will use to determine
+        // which parts of the tiles are valid / required.
+        let clip_rect = match self
+            .clip_node_collector
+            .get_world_clip_rect(
+                clip_store,
+                &resources.clip_data_store,
+                frame_context.clip_scroll_tree,
+            ) {
+            Some(clip_rect) => clip_rect,
+            None => return,
+        };
 
-        let mut dirty_rect = LayoutRect::zero();
+        let clip_rect = match clip_rect.intersection(&frame_context.screen_world_rect) {
+            Some(clip_rect) => clip_rect,
+            None => return,
+        };
+
+        let clipped = (clip_rect * frame_context.device_pixel_scale).round().to_i32();
 
         // 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_rect = self.get_tile_rect(x, y);
-                let tile = &mut self.tiles[i as usize];
+        for (i, tile) in self.tiles.iter_mut().enumerate() {
+            // Check the content of the tile is the same
+            tile.is_valid &= tile.descriptor.is_valid();
+
+            // Invalidate if the backing texture was evicted.
+            if !resource_cache.texture_cache.is_allocated(&tile.handle) {
+                tile.is_valid = false;
+            }
 
-                // 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 {
+            // Work out which part of the tile rect is valid.
+            let tile_device_rect = (tile.world_rect * frame_context.device_pixel_scale).round().to_i32();
+
+            // Get the part of the tile rect that is actually going to be rendered on screen.
+            let src_rect = clipped.intersection(&tile_device_rect);
+
+            // If that was completely off-screen, nothing to do.
+            let src_rect = match src_rect {
+                Some(rect) => rect,
+                None => {
                     continue;
                 }
+            };
 
-                // 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 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.
+            // Get this space relative to the tile itself.
+            let unit_rect = src_rect
+                .translate(&-tile_device_rect.origin.to_vector());
 
-                    if let Some(retained_handle) = retained_tiles.tiles.remove(&tile.descriptor) {
-                        // Only use if not evicted from texture cache in the meantime.
-                        if resource_cache.texture_cache.is_allocated(&retained_handle) {
-                            // We found a matching tile from the previous scene, so use it!
-                            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;
-                            }
-                        }
-                    }
+            // Work out if the currently valid rect of this tile is large
+            // enough for what we need to draw.
+            let valid_pixel_rect = match tile.pixel_rect {
+                Some(pixel_rect) => pixel_rect.contains_rect(&unit_rect),
+                None => false,
+            };
+            if !valid_pixel_rect {
+                tile.is_valid = false;
+            }
+
+            // Request the backing texture so it won't get evicted this frame.
+            resource_cache.texture_cache.request(&tile.handle, gpu_cache);
+
+            // Decide how to handle this tile when drawing this frame.
+            if tile.is_valid {
+                // If the tile is valid, we will generally want to draw it
+                // on screen. However, if there are no primitives there is
+                // no need to draw it.
+                if !tile.descriptor.prims.is_empty() {
+                    self.tiles_to_draw.push(TileIndex(i));
                 }
-
-                // Invalidate the tile if not cacheable
-                if !tile.is_cacheable {
-                    tile.is_valid = false;
-                }
+            } else {
+                // Add the tile rect to the dirty rect.
+                dirty_world_rect = dirty_world_rect.union(&tile.world_rect);
 
-                // 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 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;
-                    }
-                }
+                // Store the currently valid rect for this tile.
+                tile.pixel_rect = Some(unit_rect);
 
-                // 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;
-                }
+                // Ensure that this texture is allocated.
+                resource_cache.texture_cache.update(
+                    &mut tile.handle,
+                    descriptor,
+                    TextureFilter::Linear,
+                    None,
+                    [0.0; 3],
+                    DirtyRect::All,
+                    gpu_cache,
+                    None,
+                    UvRectKind::Rect,
+                    Eviction::Eager,
+                );
 
-                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);
+                let cache_item = resource_cache
+                    .get_texture_cache_item(&tile.handle);
 
-                    // 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 {
-                        dirty_rect = dirty_rect.union(&tile_rect);
-                        tile_offset.x = tile_offset.x.min(x);
-                        tile_offset.y = tile_offset.y.min(y);
-                    }
-                }
+                // Store a blit operation to be done after drawing the
+                // frame in order to update the cached texture tile.
+                let dest_offset = unit_rect.origin;
+                self.pending_blits.push(TileBlit {
+                    target: cache_item,
+                    src_offset: src_rect.origin,
+                    dest_offset,
+                    size: unit_rect.size,
+                });
+
+                // We can consider this tile valid now.
+                tile.is_valid = true;
             }
         }
 
-        self.dirty_region = if dirty_rect.is_empty() {
+        // Store the dirty region for drawing the main scene.
+        self.dirty_region = if dirty_world_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,
             })
         };
     }
 }
 
 /// State structure that is used during the tile cache update picture traversal.
 pub struct TileCacheUpdateState {
-    pub tile_cache: Option<(TileCache, SpatialNodeIndex)>,
+    pub tile_cache: Option<TileCache>,
 }
 
 impl TileCacheUpdateState {
     pub fn new() -> Self {
         TileCacheUpdateState {
             tile_cache: None,
         }
     }
@@ -1163,16 +982,18 @@ pub struct SurfaceInfo {
     pub raster_spatial_node_index: SpatialNodeIndex,
     pub surface_spatial_node_index: SpatialNodeIndex,
     /// This is set when the render task is created.
     pub surface: Option<PictureSurface>,
     /// A list of render tasks that are dependencies of this surface.
     pub tasks: Vec<RenderTaskId>,
     /// How much the local surface rect should be inflated (for blur radii).
     pub inflation_factor: f32,
+    /// A list of tile blits to be done after drawing this surface.
+    pub tile_blits: Vec<TileBlit>,
 }
 
 impl SurfaceInfo {
     pub fn new(
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         inflation_factor: f32,
         world_rect: WorldRect,
@@ -1197,16 +1018,17 @@ impl SurfaceInfo {
         SurfaceInfo {
             rect: PictureRect::zero(),
             map_local_to_surface,
             surface: None,
             raster_spatial_node_index,
             surface_spatial_node_index,
             tasks: Vec::new(),
             inflation_factor,
+            tile_blits: Vec::new(),
         }
     }
 
     /// Take the set of child render tasks for this surface. This is
     /// used when constructing the render task tree.
     pub fn take_render_tasks(&mut self) -> Vec<RenderTaskId> {
         mem::replace(&mut self.tasks, Vec::new())
     }
@@ -1587,24 +1409,18 @@ impl PicturePrimitive {
     /// a frame builder is replaced with a newly built scene. It
     /// gives a picture a chance to retain any cached tiles that
     /// may be useful during the next scene build.
     pub fn destroy(
         mut self,
         retained_tiles: &mut RetainedTiles,
     ) {
         if let Some(tile_cache) = self.tile_cache.take() {
-            debug_assert!(tile_cache.old_tiles.is_empty());
             for tile in tile_cache.tiles {
-                if let Some((descriptor, handle)) = tile.destroy() {
-                    retained_tiles.tiles.insert(
-                        descriptor,
-                        handle,
-                    );
-                }
+                retained_tiles.tiles.push(tile);
             }
         }
     }
 
     pub fn new_image(
         requested_composite_mode: Option<PictureCompositeMode>,
         context_3d: Picture3DContext<OrderedPictureChild>,
         pipeline_id: PipelineId,
@@ -1635,17 +1451,17 @@ impl PicturePrimitive {
                 clip_store,
             )
         } else {
             None
         };
 
         let tile_cache = match requested_composite_mode {
             Some(PictureCompositeMode::TileCache { .. }) => {
-                Some(TileCache::new())
+                Some(TileCache::new(spatial_node_index))
             }
             Some(_) | None => {
                 None
             }
         };
 
         PicturePrimitive {
             surface_desc,
@@ -1672,16 +1488,17 @@ impl PicturePrimitive {
         &mut self,
         pic_index: PictureIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         surface_index: SurfaceIndex,
         parent_allows_subpixel_aa: bool,
         frame_state: &mut FrameBuildingState,
         frame_context: &FrameBuildingContext,
+        dirty_world_rect: WorldRect,
     ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
         if !self.is_visible() {
             return None;
         }
 
         // Work out the dirty world rect for this picture.
         let dirty_world_rect = match self.tile_cache {
             Some(ref tile_cache) => {
@@ -1692,18 +1509,18 @@ impl PicturePrimitive {
                 tile_cache
                     .dirty_region
                     .as_ref()
                     .map_or(WorldRect::zero(), |region| {
                         region.dirty_world_rect
                     })
             }
             None => {
-                // No tile cache - just assume the world rect of the screen.
-                frame_context.screen_world_rect
+                // No tile cache - just assume the current dirty world rect.
+                dirty_world_rect
             }
         };
 
         // Extract the raster and surface spatial nodes from the raster
         // config, if this picture establishes a surface. Otherwise just
         // pass in the spatial node indices from the parent context.
         let (raster_spatial_node_index, surface_spatial_node_index, surface_index) = match self.raster_config {
             Some(ref raster_config) => {
@@ -2032,30 +1849,28 @@ impl PicturePrimitive {
         Some(mem::replace(&mut self.prim_list.pictures, SmallVec::new()))
     }
 
     /// Update the primitive dependencies for any active tile caches,
     /// but only *if* the transforms have made the mappings out of date.
     pub fn update_prim_dependencies(
         &self,
         tile_cache: &mut TileCache,
-        surface_spatial_node_index: SpatialNodeIndex,
         frame_context: &FrameBuildingContext,
         resource_cache: &mut ResourceCache,
         resources: &FrameResources,
         pictures: &[PicturePrimitive],
         clip_store: &ClipStore,
         opacity_binding_store: &OpacityBindingStorage,
         image_instances: &ImageInstanceStorage,
     ) {
         for prim_instance in &self.prim_list.prim_instances {
             tile_cache.update_prim_dependencies(
                 prim_instance,
                 &self.prim_list,
-                surface_spatial_node_index,
                 &frame_context.clip_scroll_tree,
                 resources,
                 &clip_store.clip_chain_nodes,
                 pictures,
                 resource_cache,
                 opacity_binding_store,
                 image_instances,
             );
@@ -2149,35 +1964,16 @@ impl PicturePrimitive {
         self.prim_list.pictures = child_pictures;
 
         // If this picture establishes a surface, then map the surface bounding
         // rect into the parent surface coordinate space, and propagate that up
         // to the parent.
         if let Some(ref mut raster_config) = self.raster_config {
             let surface_rect = state.current_surface().rect;
 
-            // Sometimes, Gecko supplies a huge picture rect. This is typically
-            // due to some weird startup condition, or a reftest that has an
-            // extreme scale in it. In these cases, disable the tile cache and
-            // just use the normal Blit composite mode for this picture. This
-            // is not ideal (we could skip the surface altogether in the future)
-            // but it's simple and works around this edge case for now.
-            if let Some(ref mut tile_cache) = self.tile_cache {
-                if surface_rect.size.width > MAX_PICTURE_SIZE ||
-                   surface_rect.size.height > MAX_PICTURE_SIZE ||
-                   surface_rect.size.width <= 0.0 ||
-                   surface_rect.size.height <= 0.0
-                {
-                    tile_cache.needs_update = false;
-                    tile_cache.tiles.clear();
-                    tile_cache.tile_rect = TileRect::zero();
-                    raster_config.composite_mode = PictureCompositeMode::Blit;
-                }
-            }
-
             let mut surface_rect = TypedRect::from_untyped(&surface_rect.to_untyped());
 
             // Pop this surface from the stack
             state.pop_surface();
 
             // If the local rect changed (due to transforms in child primitives) then
             // invalidate the GPU cache location to re-upload the new local rect
             // and stretch size. Drop shadow filters also depend on the local rect
@@ -2272,134 +2068,26 @@ impl PicturePrimitive {
         // TODO(gw): Almost all of the Picture types below use extra_gpu_cache_data
         //           to store the same type of data. The exception is the filter
         //           with a ColorMatrix, which stores the color matrix here. It's
         //           probably worth tidying this code up to be a bit more consistent.
         //           Perhaps store the color matrix after the common data, even though
         //           it's not used by that shader.
 
         let surface = match raster_config.composite_mode {
-            PictureCompositeMode::TileCache { clear_color, .. } => {
+            PictureCompositeMode::TileCache { .. } => {
                 let tile_cache = self.tile_cache.as_mut().unwrap();
 
-                // Build the render task for a tile cache picture, if there is
-                // any dirty rect.
-
-                match tile_cache.dirty_region {
-                    Some(ref dirty_region) => {
-                        // Texture cache descriptor for each tile.
-                        // TODO(gw): If / when we start to use tile caches with
-                        //           clip masks and/or transparent backgrounds,
-                        //           we will need to correctly select an opacity
-                        //           here and a blend mode in batch.rs.
-                        let descriptor = ImageDescriptor::new(
-                            TILE_SIZE_WIDTH,
-                            TILE_SIZE_HEIGHT,
-                            ImageFormat::BGRA8,
-                            true,
-                            false,
-                        );
-
-                        // Get a picture rect, expanded to tile boundaries.
-                        let p0 = pic_rect.origin;
-                        let p1 = pic_rect.bottom_right();
-                        let local_tile_size = tile_cache.local_tile_size;
-                        let aligned_pic_rect = PictureRect::from_floats(
-                            (p0.x / local_tile_size.width).floor() * local_tile_size.width,
-                            (p0.y / local_tile_size.height).floor() * local_tile_size.height,
-                            (p1.x / local_tile_size.width).ceil() * local_tile_size.width,
-                            (p1.y / local_tile_size.height).ceil() * local_tile_size.height,
-                        );
-
-                        let mut blits = Vec::new();
-
-                        // 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 && 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::Eager,
-                                    );
+                // For a picture surface, just push any child tasks and tile
+                // blits up to the parent surface.
+                let surface = &mut surfaces[surface_index.0];
+                surface.tasks.extend(child_tasks);
+                surface.tile_blits.extend(tile_cache.pending_blits.drain(..));
 
-                                    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.
-                                    let offset = DeviceIntPoint::new(
-                                        (x - dirty_region.tile_offset.x) * TILE_SIZE_WIDTH,
-                                        (y - dirty_region.tile_offset.y) * TILE_SIZE_HEIGHT,
-                                    );
-
-                                    blits.push(TileBlit {
-                                        target: cache_item,
-                                        offset,
-                                    });
-
-                                    tile.is_valid = true;
-                                }
-                            }
-                        }
-
-                        // We want to clip the drawing of this and any children to the
-                        // dirty rect.
-                        let clipped_rect = dirty_region.dirty_world_rect;
-
-                        let (clipped, unclipped) = match get_raster_rects(
-                            aligned_pic_rect,
-                            &map_pic_to_raster,
-                            &map_raster_to_world,
-                            clipped_rect,
-                            frame_context.device_pixel_scale,
-                        ) {
-                            Some(info) => info,
-                            None => {
-                                return false;
-                            }
-                        };
-
-                        let picture_task = RenderTask::new_picture(
-                            RenderTaskLocation::Dynamic(None, clipped.size),
-                            unclipped.size,
-                            pic_index,
-                            clipped.origin,
-                            child_tasks,
-                            UvRectKind::Rect,
-                            pic_context.raster_spatial_node_index,
-                            Some(clear_color),
-                            blits,
-                        );
-
-                        let render_task_id = frame_state.render_tasks.add(picture_task);
-                        surfaces[surface_index.0].tasks.push(render_task_id);
-
-                        PictureSurface::RenderTask(render_task_id)
-                    }
-                    None => {
-                        // None of the tiles have changed, so we can skip any drawing!
-                        return true;
-                    }
-                }
+                return true;
             }
             PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)) => {
                 let blur_std_deviation = blur_radius * frame_context.device_pixel_scale.0;
                 let blur_range = (blur_std_deviation * BLUR_SAMPLE_SCALE).ceil() as i32;
 
                 // We need to choose whether to cache this picture, or draw
                 // it into a temporary render target each frame. If we draw
                 // it into a persistently cached texture, then we want to
@@ -2833,33 +2521,8 @@ fn create_raster_mappers(
         raster_spatial_node_index,
         surface_spatial_node_index,
         raster_bounds,
         clip_scroll_tree,
     );
 
     (map_raster_to_world, map_pic_to_raster)
 }
-
-// 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/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -1679,39 +1679,34 @@ impl PrimitiveStore {
             // composite mode of this picture. In some cases, even if the requested
             // composite mode was tile caching, WR may choose not to draw this picture
             // with tile cache enabled. For now, this is only in the case of very large
             // picture rects, but in future we may do it for performance reasons too.
             if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) = pic.raster_config {
                 debug_assert!(state.tile_cache.is_none());
                 let mut tile_cache = pic.tile_cache.take().unwrap();
 
-                let surface_index = pic.raster_config.as_ref().unwrap().surface_index;
-                let surface = &surfaces[surface_index.0];
-
                 // If we have a tile cache for this picture, see if any of the
                 // relative transforms have changed, which means we need to
                 // re-map the dependencies of any child primitives.
-                tile_cache.update_transforms(
-                    surface.surface_spatial_node_index,
-                    surface.raster_spatial_node_index,
-                    pic.requested_raster_space,
+                tile_cache.pre_update(
+                    pic.local_rect,
                     frame_context,
-                    pic.local_rect,
+                    resource_cache,
+                    retained_tiles,
                 );
 
-                state.tile_cache = Some((tile_cache, pic.spatial_node_index));
+                state.tile_cache = Some(tile_cache);
             }
             mem::replace(&mut pic.prim_list.pictures, SmallVec::new())
         };
 
-        if let Some((ref mut tile_cache, surface_spatial_node_index)) = state.tile_cache {
+        if let Some(ref mut tile_cache) = state.tile_cache {
             self.pictures[pic_index.0].update_prim_dependencies(
                 tile_cache,
-                surface_spatial_node_index,
                 frame_context,
                 resource_cache,
                 resources,
                 &self.pictures,
                 clip_store,
                 &self.opacity_bindings,
                 &self.images,
             );
@@ -1728,29 +1723,30 @@ impl PrimitiveStore {
                 surfaces,
                 gpu_cache,
                 retained_tiles,
             );
         }
 
         let pic = &mut self.pictures[pic_index.0];
         if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) = pic.raster_config {
-            let mut tile_cache = state.tile_cache.take().unwrap().0;
+            let mut tile_cache = state.tile_cache.take().unwrap();
 
             // Build the dirty region(s) for this tile cache.
-            tile_cache.build_dirty_regions(
-                pic.spatial_node_index,
-                frame_context,
+            tile_cache.post_update(
                 resource_cache,
                 gpu_cache,
-                retained_tiles,
+                clip_store,
+                frame_context,
+                resources,
             );
 
             pic.tile_cache = Some(tile_cache);
         }
+
         pic.prim_list.pictures = children;
     }
 
     pub fn get_opacity_binding(
         &self,
         opacity_binding_index: OpacityBindingIndex,
     ) -> f32 {
         if opacity_binding_index == OpacityBindingIndex::INVALID {
@@ -1899,16 +1895,17 @@ impl PrimitiveStore {
                     match pic.take_context(
                         pic_index,
                         pic_context.surface_spatial_node_index,
                         pic_context.raster_spatial_node_index,
                         pic_context.surface_index,
                         pic_context.allow_subpixel_aa,
                         frame_state,
                         frame_context,
+                        pic_context.dirty_world_rect,
                     ) {
                         Some(info) => Some(info),
                         None => {
                             if prim_instance.is_chased() {
                                 println!("\tculled for carrying an invisible composite filter");
                             }
 
                             return false;
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -250,17 +250,19 @@ pub struct ClipRegionTask {
     pub local_pos: LayoutPoint,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct TileBlit {
     pub target: CacheItem,
-    pub offset: DeviceIntPoint,
+    pub src_offset: DeviceIntPoint,
+    pub dest_offset: DeviceIntPoint,
+    pub size: DeviceIntSize,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PictureTask {
     pub pic_index: PictureIndex,
     pub can_merge: bool,
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -1597,16 +1597,18 @@ pub struct Renderer {
     /// List of profile results from previous frames. Can be retrieved
     /// via get_frame_profiles().
     cpu_profiles: VecDeque<CpuProfile>,
     gpu_profiles: VecDeque<GpuProfile>,
 
     /// Notification requests to be fulfilled after rendering.
     notifications: Vec<NotificationRequest>,
 
+    framebuffer_size: Option<DeviceIntSize>,
+
     #[cfg(feature = "capture")]
     read_fbo: FBOId,
     #[cfg(feature = "replay")]
     owned_external_images: FastHashMap<(ExternalImageId, u8), ExternalTexture>,
 }
 
 #[derive(Debug)]
 pub enum RendererError {
@@ -2043,16 +2045,17 @@ impl Renderer {
             texture_cache_upload_pbo,
             texture_resolver,
             renderer_errors: Vec::new(),
             #[cfg(feature = "capture")]
             read_fbo,
             #[cfg(feature = "replay")]
             owned_external_images: FastHashMap::default(),
             notifications: Vec::new(),
+            framebuffer_size: None,
         };
 
         renderer.set_debug_flags(options.debug_flags);
 
         let sender = RenderApiSender::new(api_tx, payload_tx);
         Ok((renderer, sender))
     }
 
@@ -2111,17 +2114,18 @@ impl Renderer {
                     // Add a new document to the active set, expressed as a `Vec` in order
                     // to re-order based on `DocumentLayer` during rendering.
                     match self.active_documents.iter().position(|&(id, _)| id == document_id) {
                         Some(pos) => {
                             // If the document we are replacing must be drawn
                             // (in order to update the texture cache), issue
                             // a render just to off-screen targets.
                             if self.active_documents[pos].1.frame.must_be_drawn() {
-                                self.render_impl(None).ok();
+                                let framebuffer_size = self.framebuffer_size;
+                                self.render_impl(framebuffer_size).ok();
                             }
                             self.active_documents[pos].1 = doc;
                         }
                         None => self.active_documents.push((document_id, doc)),
                     }
 
                     // IMPORTANT: The pending texture cache updates must be applied
                     //            *after* the previous frame has been rendered above
@@ -2478,16 +2482,18 @@ impl Renderer {
     /// Renders the current frame.
     ///
     /// A Frame is supplied by calling [`generate_frame()`][genframe].
     /// [genframe]: ../../webrender_api/struct.DocumentApi.html#method.generate_frame
     pub fn render(
         &mut self,
         framebuffer_size: DeviceIntSize,
     ) -> Result<RendererStats, Vec<RendererError>> {
+        self.framebuffer_size = Some(framebuffer_size);
+
         let result = self.render_impl(Some(framebuffer_size));
 
         drain_filter(
             &mut self.notifications,
             |n| { n.when() == Checkpoint::FrameRendered },
             |n| { n.notify(); },
         );
 
@@ -3490,29 +3496,45 @@ impl Renderer {
                     .expect("BUG: invalid target texture");
 
                 self.device.bind_draw_target(DrawTarget::Texture {
                     texture,
                     layer: blit.target.texture_layer as usize,
                     with_depth: false,
                 });
 
-                let src_rect = DeviceIntRect::new(
-                    blit.offset,
-                    blit.target.uv_rect.size.to_i32(),
+                let mut src_rect = DeviceIntRect::new(
+                    blit.src_offset,
+                    blit.size,
                 );
 
-                let dest_rect = blit.target.uv_rect.to_i32();
+                let target_rect = blit.target.uv_rect.to_i32();
+
+                let mut dest_rect = DeviceIntRect::new(
+                    DeviceIntPoint::new(
+                        blit.dest_offset.x + target_rect.origin.x,
+                        blit.dest_offset.y + target_rect.origin.y,
+                    ),
+                    blit.size,
+                );
+
+                // Modify the src/dest rects since we are blitting from the framebuffer
+                src_rect.origin.y = draw_target.dimensions().height as i32 - src_rect.size.height - src_rect.origin.y;
+                dest_rect.origin.y += dest_rect.size.height;
+                dest_rect.size.height = -dest_rect.size.height;
 
                 self.device.blit_render_target(
                     src_rect,
                     dest_rect,
                 );
             }
+
+            self.device.bind_draw_target(draw_target);
         }
+
     }
 
     fn draw_alpha_target(
         &mut self,
         draw_target: DrawTarget,
         target: &AlphaRenderTarget,
         projection: &Transform3D<f32>,
         render_tasks: &RenderTaskTree,
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -419,20 +419,22 @@ impl RenderTarget for ColorRenderTarget 
                         prim_headers,
                         transforms,
                         pic_task.root_spatial_node_index,
                         z_generator,
                     );
 
                     for blit in &pic_task.blits {
                         self.tile_blits.push(TileBlit {
+                            dest_offset: blit.dest_offset,
+                            size: blit.size,
                             target: blit.target.clone(),
-                            offset: DeviceIntPoint::new(
-                                blit.offset.x + target_rect.origin.x,
-                                blit.offset.y + target_rect.origin.y,
+                            src_offset: DeviceIntPoint::new(
+                                blit.src_offset.x + target_rect.origin.x,
+                                blit.src_offset.y + target_rect.origin.y,
                             ),
                         })
                     }
 
                     if let Some(batch_container) = batch_builder.build(&mut merged_batches) {
                         self.alpha_batch_containers.push(batch_container);
                     }
                 }
--- a/gfx/wr/webrender/src/util.rs
+++ b/gfx/wr/webrender/src/util.rs
@@ -879,8 +879,101 @@ pub fn recycle_vec<T>(vec: &mut Vec<T>) 
     if vec.capacity() > 2 * vec.len() {
         // Reduce capacity of the buffer if it is a lot larger than it needs to be. This prevents
         // a frame with exceptionally large allocations to cause subsequent frames to retain
         // more memory than they need.
         vec.shrink_to_fit();
     }
     vec.clear();
 }
+
+/// A specialized array container for comparing equality between the current
+/// contents and the new contents, incrementally. As each item is added, the
+/// container maintains track of whether this is the same as last time items
+/// were added, or if the contents have diverged. After each reset, the memory
+/// of the vec is retained, which means that memory allocation is rare.
+#[derive(Debug)]
+pub struct ComparableVec<T> {
+    /// The items to be stored and compared
+    items: Vec<T>,
+    /// The current index to add the next item to
+    current_index: usize,
+    /// The previous length of the array
+    prev_len: usize,
+    /// Whether the contents of the vec is the same as last time.
+    is_same: bool,
+}
+
+impl<T> ComparableVec<T> where T: PartialEq + Clone + fmt::Debug {
+    /// Construct a new comparable vec
+    pub fn new() -> Self {
+        ComparableVec {
+            items: Vec::new(),
+            current_index: 0,
+            prev_len: 0,
+            is_same: false,
+        }
+    }
+
+    /// Retrieve a reference to the current items array
+    pub fn items(&self) -> &[T] {
+        &self.items[.. self.current_index]
+    }
+
+    /// Clear the contents of the vec, ready for adding new items.
+    pub fn reset(&mut self) {
+        self.items.truncate(self.current_index);
+        self.prev_len = self.current_index;
+        self.current_index = 0;
+        self.is_same = true;
+    }
+
+    /// Return the current length of the container
+    pub fn len(&self) -> usize {
+        self.current_index
+    }
+
+    /// Return true if the container has no items
+    pub fn is_empty(&self) -> bool {
+        self.current_index == 0
+    }
+
+    /// Push a number of items into the container
+    pub fn extend_from_slice(&mut self, items: &[T]) {
+        for item in items {
+            self.push(item.clone());
+        }
+    }
+
+    /// Push a single item into the container.
+    pub fn push(&mut self, item: T) {
+        // If this item extends the real length of the vec, it's clearly not
+        // the same as last time.
+        if self.current_index < self.items.len() {
+            // If the vec is currently considered equal, we need to compare
+            // the item being pushed.
+            if self.is_same {
+                let existing_item = &mut self.items[self.current_index];
+                if *existing_item != item {
+                    // Overwrite the current item with the new one and
+                    // mark the vec as different.
+                    *existing_item = item;
+                    self.is_same = false;
+                }
+            } else {
+                // The vec is already not equal, so just push the item.
+                self.items[self.current_index] = item;
+            }
+        } else {
+            // In this case, mark the vec as different and store the new item.
+            self.is_same = false;
+            self.items.push(item);
+        }
+
+        // Increment where the next item will be pushed.
+        self.current_index += 1;
+    }
+
+    /// Return true if the contents of the vec are the same as the previous time.
+    pub fn is_valid(&self) -> bool {
+        self.is_same && self.prev_len == self.current_index
+    }
+}