Bug 1509498 - Update webrender to commit 523be3a9461de2716828cd2271aabaffc5e4caa0 (WR PR #3332). r=kats
authorWR Updater Bot <graphics-team@mozilla.staktrace.com>
Fri, 23 Nov 2018 14:43:06 +0000
changeset 504250 39c6967a31f420efee4ad0ba0ddc224219aca8a9
parent 504249 ab0838a6a61bbcba9b45a622ee16b4ea744ec712
child 504251 381febb2c4045e79c4646f8a20079d1f1eb1f00f
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats
bugs1509498
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1509498 - Update webrender to commit 523be3a9461de2716828cd2271aabaffc5e4caa0 (WR PR #3332). r=kats https://github.com/servo/webrender/pull/3332 Differential Revision: https://phabricator.services.mozilla.com/D12754
gfx/webrender_bindings/revision.txt
gfx/wr/examples/animation.rs
gfx/wr/examples/common/boilerplate.rs
gfx/wr/webrender/res/brush.glsl
gfx/wr/webrender/res/brush_image.glsl
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store.rs
gfx/wr/webrender/src/render_task.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/resource_cache.rs
gfx/wr/webrender/src/scene.rs
gfx/wr/webrender/src/surface.rs
gfx/wr/webrender/src/tiling.rs
gfx/wr/wrench/src/reftest.rs
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-b4f7e431d56d35051085b4f9486f1cee3dd7339b
+523be3a9461de2716828cd2271aabaffc5e4caa0
--- a/gfx/wr/examples/animation.rs
+++ b/gfx/wr/examples/animation.rs
@@ -19,134 +19,172 @@ extern crate winit;
 #[path = "common/boilerplate.rs"]
 mod boilerplate;
 
 use boilerplate::{Example, HandyDandyRectBuilder};
 use euclid::Angle;
 use webrender::api::*;
 
 struct App {
-    property_key: PropertyBindingKey<LayoutTransform>,
+    property_key0: PropertyBindingKey<LayoutTransform>,
+    property_key1: PropertyBindingKey<LayoutTransform>,
+    property_key2: PropertyBindingKey<LayoutTransform>,
     opacity_key: PropertyBindingKey<f32>,
-    transform: LayoutTransform,
     opacity: f32,
+    angle0: f32,
+    angle1: f32,
+    angle2: f32,
+}
+
+impl App {
+    fn add_rounded_rect(
+        &mut self,
+        bounds: LayoutRect,
+        color: ColorF,
+        builder: &mut DisplayListBuilder,
+        property_key: PropertyBindingKey<LayoutTransform>,
+    ) {
+        let filters = [
+            FilterOp::Opacity(PropertyBinding::Binding(self.opacity_key, self.opacity), self.opacity),
+        ];
+
+        let reference_frame_id = builder.push_reference_frame(
+            &LayoutPrimitiveInfo::new(LayoutRect::new(bounds.origin, LayoutSize::zero())),
+            Some(PropertyBinding::Binding(property_key, LayoutTransform::identity())),
+            None,
+        );
+
+        builder.push_clip_id(reference_frame_id);
+
+        builder.push_stacking_context(
+            &LayoutPrimitiveInfo::new(LayoutRect::zero()),
+            None,
+            TransformStyle::Flat,
+            MixBlendMode::Normal,
+            &filters,
+            RasterSpace::Screen,
+        );
+
+        let clip_bounds = LayoutRect::new(LayoutPoint::zero(), bounds.size);
+        let complex_clip = ComplexClipRegion {
+            rect: clip_bounds,
+            radii: BorderRadius::uniform(30.0),
+            mode: ClipMode::Clip,
+        };
+        let clip_id = builder.define_clip(clip_bounds, vec![complex_clip], None);
+        builder.push_clip_id(clip_id);
+
+        // Fill it with a white rect
+        builder.push_rect(
+            &LayoutPrimitiveInfo::new(LayoutRect::new(LayoutPoint::zero(), bounds.size)),
+            color,
+        );
+
+        builder.pop_clip_id();
+
+        builder.pop_stacking_context();
+
+        builder.pop_clip_id();
+        builder.pop_reference_frame();
+    }
 }
 
 impl Example for App {
+    const WIDTH: u32 = 1024;
+    const HEIGHT: u32 = 1024;
+
     fn render(
         &mut self,
         _api: &RenderApi,
         builder: &mut DisplayListBuilder,
         _txn: &mut Transaction,
         _framebuffer_size: DeviceIntSize,
         _pipeline_id: PipelineId,
         _document_id: DocumentId,
     ) {
-        // Create a 200x200 stacking context with an animated transform property.
-        let bounds = (0, 0).to(200, 200);
-
-        let filters = [
-            FilterOp::Opacity(PropertyBinding::Binding(self.opacity_key, self.opacity), self.opacity),
-        ];
-
-        let info = LayoutPrimitiveInfo::new(bounds);
-        let reference_frame_id = builder.push_reference_frame(
-            &info,
-            Some(PropertyBinding::Binding(self.property_key, LayoutTransform::identity())),
-            None,
-        );
-
-        builder.push_clip_id(reference_frame_id);
+        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);
 
-        let info = LayoutPrimitiveInfo::new(bounds);
-        builder.push_stacking_context(
-            &info,
-            None,
-            TransformStyle::Flat,
-            MixBlendMode::Normal,
-            &filters,
-            RasterSpace::Screen,
-        );
+        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);
 
-        let complex_clip = ComplexClipRegion {
-            rect: bounds,
-            radii: BorderRadius::uniform(50.0),
-            mode: ClipMode::Clip,
-        };
-        let clip_id = builder.define_clip(bounds, vec![complex_clip], None);
-        builder.push_clip_id(clip_id);
-
-        // Fill it with a white rect
-        builder.push_rect(&info, ColorF::new(0.0, 1.0, 0.0, 1.0));
-
-        builder.pop_clip_id();
-
-        builder.pop_stacking_context();
-
-        builder.pop_clip_id();
-        builder.pop_reference_frame();
+        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);
     }
 
     fn on_event(&mut self, win_event: winit::WindowEvent, api: &RenderApi, document_id: DocumentId) -> bool {
         match win_event {
             winit::WindowEvent::KeyboardInput {
                 input: winit::KeyboardInput {
                     state: winit::ElementState::Pressed,
                     virtual_keycode: Some(key),
                     ..
                 },
                 ..
             } => {
-                let (offset_x, offset_y, angle, delta_opacity) = match key {
-                    winit::VirtualKeyCode::Down => (0.0, 10.0, 0.0, 0.0),
-                    winit::VirtualKeyCode::Up => (0.0, -10.0, 0.0, 0.0),
-                    winit::VirtualKeyCode::Right => (10.0, 0.0, 0.0, 0.0),
-                    winit::VirtualKeyCode::Left => (-10.0, 0.0, 0.0, 0.0),
-                    winit::VirtualKeyCode::Comma => (0.0, 0.0, 0.1, 0.0),
-                    winit::VirtualKeyCode::Period => (0.0, 0.0, -0.1, 0.0),
-                    winit::VirtualKeyCode::Z => (0.0, 0.0, 0.0, -0.1),
-                    winit::VirtualKeyCode::X => (0.0, 0.0, 0.0, 0.1),
+                let (delta_angle, delta_opacity) = match key {
+                    winit::VirtualKeyCode::Down => (0.0, -0.1),
+                    winit::VirtualKeyCode::Up => (0.0, 0.1),
+                    winit::VirtualKeyCode::Right => (1.0, 0.0),
+                    winit::VirtualKeyCode::Left => (-1.0, 0.0),
                     _ => return false,
                 };
                 // Update the transform based on the keyboard input and push it to
                 // webrender using the generate_frame API. This will recomposite with
                 // the updated transform.
                 self.opacity += delta_opacity;
-                let new_transform = self.transform
-                    .pre_rotate(0.0, 0.0, 1.0, Angle::radians(angle))
-                    .post_translate(LayoutVector3D::new(offset_x, offset_y, 0.0));
+                self.angle0 += delta_angle * 0.1;
+                self.angle1 += delta_angle * 0.2;
+                self.angle2 -= delta_angle * 0.15;
+                let xf0 = LayoutTransform::create_rotation(0.0, 0.0, 1.0, Angle::radians(self.angle0));
+                let xf1 = LayoutTransform::create_rotation(0.0, 0.0, 1.0, Angle::radians(self.angle1));
+                let xf2 = LayoutTransform::create_rotation(0.0, 0.0, 1.0, Angle::radians(self.angle2));
                 let mut txn = Transaction::new();
                 txn.update_dynamic_properties(
                     DynamicProperties {
                         transforms: vec![
                             PropertyValue {
-                                key: self.property_key,
-                                value: new_transform,
+                                key: self.property_key0,
+                                value: xf0,
+                            },
+                            PropertyValue {
+                                key: self.property_key1,
+                                value: xf1,
+                            },
+                            PropertyValue {
+                                key: self.property_key2,
+                                value: xf2,
                             },
                         ],
                         floats: vec![
                             PropertyValue {
                                 key: self.opacity_key,
                                 value: self.opacity,
                             }
                         ],
                     },
                 );
                 txn.generate_frame();
                 api.send_transaction(document_id, txn);
-                self.transform = new_transform;
             }
             _ => (),
         }
 
         false
     }
 }
 
 fn main() {
     let mut app = App {
-        property_key: PropertyBindingKey::new(42), // arbitrary magic number
+        property_key0: PropertyBindingKey::new(42), // arbitrary magic number
+        property_key1: PropertyBindingKey::new(44), // arbitrary magic number
+        property_key2: PropertyBindingKey::new(45), // arbitrary magic number
         opacity_key: PropertyBindingKey::new(43),
-        transform: LayoutTransform::create_translation(0.0, 0.0, 0.0),
         opacity: 0.5,
+        angle0: 0.0,
+        angle1: 0.0,
+        angle2: 0.0,
     };
     boilerplate::main_wrapper(&mut app, None);
 }
--- a/gfx/wr/examples/common/boilerplate.rs
+++ b/gfx/wr/examples/common/boilerplate.rs
@@ -174,33 +174,35 @@ pub fn main_wrapper<E: Example>(
     if let Some(output_image_handler) = output {
         renderer.set_output_image_handler(output_image_handler);
     }
 
     if let Some(external_image_handler) = external {
         renderer.set_external_image_handler(external_image_handler);
     }
 
+    renderer.toggle_debug_flags(webrender::DebugFlags::TEXTURE_CACHE_DBG);
+
     let epoch = Epoch(0);
     let pipeline_id = PipelineId(0, 0);
     let layout_size = framebuffer_size.to_f32() / euclid::TypedScale::new(device_pixel_ratio);
     let mut builder = DisplayListBuilder::new(pipeline_id, layout_size);
     let mut txn = Transaction::new();
 
     example.render(
         &api,
         &mut builder,
         &mut txn,
         framebuffer_size,
         pipeline_id,
         document_id,
     );
     txn.set_display_list(
         epoch,
-        None,
+        Some(ColorF::new(0.3, 0.0, 0.0, 1.0)),
         layout_size,
         builder.finalize(),
         true,
     );
     txn.set_root_pipeline(pipeline_id);
     txn.generate_frame();
     api.send_transaction(document_id, txn);
 
@@ -288,17 +290,17 @@ pub fn main_wrapper<E: Example>(
                 &mut builder,
                 &mut txn,
                 framebuffer_size,
                 pipeline_id,
                 document_id,
             );
             txn.set_display_list(
                 epoch,
-                None,
+                Some(ColorF::new(0.3, 0.0, 0.0, 1.0)),
                 layout_size,
                 builder.finalize(),
                 true,
             );
             txn.generate_frame();
         }
         api.send_transaction(document_id, txn);
 
--- a/gfx/wr/webrender/res/brush.glsl
+++ b/gfx/wr/webrender/res/brush.glsl
@@ -19,46 +19,56 @@ void brush_vs(
 #define VECS_PER_SEGMENT                    2
 
 #define BRUSH_FLAG_PERSPECTIVE_INTERPOLATION    1
 #define BRUSH_FLAG_SEGMENT_RELATIVE             2
 #define BRUSH_FLAG_SEGMENT_REPEAT_X             4
 #define BRUSH_FLAG_SEGMENT_REPEAT_Y             8
 #define BRUSH_FLAG_TEXEL_RECT                  16
 
+#define INVALID_SEGMENT_INDEX                   0xffff
+
 void main(void) {
     // Load the brush instance from vertex attributes.
     int prim_header_address = aData.x;
     int clip_address = aData.y;
     int segment_index = aData.z & 0xffff;
     int edge_flags = (aData.z >> 16) & 0xff;
     int brush_flags = (aData.z >> 24) & 0xff;
     int segment_user_data = aData.w;
     PrimitiveHeader ph = fetch_prim_header(prim_header_address);
 
     // Fetch the segment of this brush primitive we are drawing.
-    int segment_address = ph.specific_prim_address +
-                          VECS_PER_SPECIFIC_BRUSH +
-                          segment_index * VECS_PER_SEGMENT;
+    vec4 segment_data;
+    RectWithSize segment_rect;
+    if (segment_index == INVALID_SEGMENT_INDEX) {
+        segment_rect = ph.local_rect;
+        segment_data = vec4(0.0);
+    } else {
+        int segment_address = ph.specific_prim_address +
+                              VECS_PER_SPECIFIC_BRUSH +
+                              segment_index * VECS_PER_SEGMENT;
 
-    vec4[2] segment_data = fetch_from_gpu_cache_2(segment_address);
-    RectWithSize local_segment_rect = RectWithSize(segment_data[0].xy, segment_data[0].zw);
+        vec4[2] segment_info = fetch_from_gpu_cache_2(segment_address);
+        segment_rect = RectWithSize(segment_info[0].xy, segment_info[0].zw);
+        segment_data = segment_info[1];
+    }
 
     VertexInfo vi;
 
     // Fetch the dynamic picture that we are drawing on.
     PictureTask pic_task = fetch_picture_task(ph.render_task_index);
     ClipArea clip_area = fetch_clip_area(clip_address);
 
     Transform transform = fetch_transform(ph.transform_id);
 
     // Write the normal vertex information out.
     if (transform.is_axis_aligned) {
         vi = write_vertex(
-            local_segment_rect,
+            segment_rect,
             ph.local_clip_rect,
             ph.z,
             transform,
             pic_task,
             ph.local_rect
         );
 
         // TODO(gw): transform bounds may be referenced by
@@ -69,17 +79,17 @@ void main(void) {
         //           more items to be brush shaders.
 #ifdef WR_FEATURE_ALPHA_PASS
         init_transform_vs(vec4(vec2(-1.0e16), vec2(1.0e16)));
 #endif
     } else {
         bvec4 edge_mask = notEqual(edge_flags & ivec4(1, 2, 4, 8), ivec4(0));
 
         vi = write_transform_vertex(
-            local_segment_rect,
+            segment_rect,
             ph.local_rect,
             ph.local_clip_rect,
             mix(vec4(0.0), vec4(1.0), edge_mask),
             ph.z,
             transform,
             pic_task
         );
     }
@@ -98,22 +108,22 @@ void main(void) {
     );
 #endif
 
     // Run the specific brush VS code to write interpolators.
     brush_vs(
         vi,
         ph.specific_prim_address,
         ph.local_rect,
-        local_segment_rect,
+        segment_rect,
         ivec4(ph.user_data, segment_user_data),
         transform.m,
         pic_task,
         brush_flags,
-        segment_data[1]
+        segment_data
     );
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 
 struct Fragment {
     vec4 color;
--- a/gfx/wr/webrender/res/brush_image.glsl
+++ b/gfx/wr/webrender/res/brush_image.glsl
@@ -68,16 +68,19 @@ void brush_vs(
 #endif
 
     ImageResource res = fetch_image_resource(user_data.w);
     vec2 uv0 = res.uv_rect.p0;
     vec2 uv1 = res.uv_rect.p1;
 
     RectWithSize local_rect = prim_rect;
     vec2 stretch_size = image_data.stretch_size;
+    if (stretch_size.x < 0.0) {
+        stretch_size = local_rect.size;
+    }
 
     // If this segment should interpolate relative to the
     // segment, modify the parameters for that.
     if ((brush_flags & BRUSH_FLAG_SEGMENT_RELATIVE) != 0) {
         local_rect = segment_rect;
         stretch_size = local_rect.size;
 
         if ((brush_flags & BRUSH_FLAG_SEGMENT_REPEAT_X) != 0) {
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AlphaType, ClipMode, DeviceIntRect, DeviceIntPoint, DeviceIntSize};
-use api::{ExternalImageType, FilterOp, ImageRendering};
-use api::{YuvColorSpace, YuvFormat, PictureRect, ColorDepth};
+use api::{ExternalImageType, FilterOp, ImageRendering, LayoutRect, LayoutSize};
+use api::{YuvColorSpace, YuvFormat, PictureRect, ColorDepth, LayoutPoint};
 use clip::{ClipDataStore, ClipNodeFlags, ClipNodeRange, ClipItem, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use glyph_rasterizer::GlyphFormat;
 use gpu_cache::{GpuCache, GpuCacheHandle, GpuCacheAddress};
 use gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders, ZBufferId, ZBufferIdGenerator};
 use gpu_types::{ClipMaskInstance, SplitCompositeInstance};
 use gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
 use gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
@@ -28,16 +28,19 @@ use smallvec::SmallVec;
 use std::{f32, i32, usize};
 use tiling::{RenderTargetContext};
 use util::{TransformedRectKind};
 
 // Special sentinel value recognized by the shader. It is considered to be
 // a dummy task that doesn't mask out anything.
 const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
 
+/// Used to signal there are no segments provided with this primitive.
+const INVALID_SEGMENT_INDEX: i32 = 0xffff;
+
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum BrushBatchKind {
     Solid,
     Image(ImageBufferKind),
     Blend,
     MixBlend {
@@ -889,22 +892,114 @@ impl AlphaBatchBuilder {
                     // hierarchy, since we process them with the root.
                     Picture3DContext::In { root_data: None, .. } => return,
                     // Proceed for non-3D pictures.
                     Picture3DContext::Out => ()
                 }
 
                 match picture.raster_config {
                     Some(ref raster_config) => {
-                        let surface = ctx.surfaces[raster_config.surface_index.0]
-                            .surface
-                            .as_ref()
-                            .expect("bug: surface must be allocated by now");
                         match raster_config.composite_mode {
+                            PictureCompositeMode::TileCache { .. } => {
+
+                                // Step through each tile in the cache, and draw it with an image
+                                // brush primitive if visible.
+
+                                let kind = BatchKind::Brush(
+                                    BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
+                                );
+
+                                let tile_cache = picture.tile_cache.as_ref().unwrap();
+
+                                for y in 0 .. tile_cache.tile_rect.size.height {
+                                    for x in 0 .. tile_cache.tile_rect.size.width {
+                                        let i = y * tile_cache.tile_rect.size.width + x;
+                                        let tile = &tile_cache.tiles[i as usize];
+
+                                        // Check if the tile is visible.
+                                        if !tile.is_visible {
+                                            continue;
+                                        }
+
+                                        // Get the local rect of the tile.
+                                        let tile_rect = LayoutRect::new(
+                                            LayoutPoint::new(
+                                                (tile_cache.tile_rect.origin.x + x) as f32 * tile_cache.local_tile_size.width,
+                                                (tile_cache.tile_rect.origin.y + y) as f32 * tile_cache.local_tile_size.height,
+                                            ),
+                                            LayoutSize::new(
+                                                tile_cache.local_tile_size.width,
+                                                tile_cache.local_tile_size.height,
+                                            ),
+                                        );
+
+                                        // 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 prim_header = PrimitiveHeader {
+                                            local_rect: tile_rect,
+                                            local_clip_rect,
+                                            task_address,
+                                            specific_prim_address: prim_cache_address,
+                                            clip_task_address,
+                                            transform_id,
+                                        };
+
+                                        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),
+                                        ]);
+
+                                        let cache_item = ctx
+                                            .resource_cache
+                                            .get_texture_cache_item(&tile.handle);
+
+                                        let key = BatchKey::new(
+                                            kind,
+                                            non_segmented_blend_mode,
+                                            BatchTextures::color(cache_item.texture_id),
+                                        );
+
+                                        let uv_rect_address = gpu_cache
+                                            .get_address(&cache_item.uv_rect_handle)
+                                            .as_int();
+
+                                        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,
+                                        };
+
+                                        // 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,
+                                        );
+
+                                        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());
                                 match filter {
                                     FilterOp::Blur(..) => {
                                         let kind = BatchKind::Brush(
                                             BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
                                         );
                                         let (uv_rect_address, textures) = surface
                                             .resolve(
@@ -1112,16 +1207,20 @@ impl AlphaBatchBuilder {
                                             bounding_rect,
                                             z_id,
                                             PrimitiveInstanceData::from(instance),
                                         );
                                     }
                                 }
                             }
                             PictureCompositeMode::MixBlend(mode) => {
+                                let surface = ctx.surfaces[raster_config.surface_index.0]
+                                    .surface
+                                    .as_ref()
+                                    .expect("bug: surface must be allocated by now");
                                 let cache_task_id = surface.resolve_render_task_id();
                                 let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
 
                                 let key = BatchKey::new(
                                     BatchKind::Brush(
                                         BrushBatchKind::MixBlend {
                                             task_id,
                                             source_id: cache_task_id,
@@ -1151,16 +1250,20 @@ impl AlphaBatchBuilder {
                                 self.batch_list.push_single_instance(
                                     key,
                                     bounding_rect,
                                     z_id,
                                     PrimitiveInstanceData::from(instance),
                                 );
                             }
                             PictureCompositeMode::Blit => {
+                                let surface = ctx.surfaces[raster_config.surface_index.0]
+                                    .surface
+                                    .as_ref()
+                                    .expect("bug: surface must be allocated by now");
                                 let cache_task_id = surface.resolve_render_task_id();
                                 let kind = BatchKind::Brush(
                                     BrushBatchKind::Image(ImageBufferKind::Texture2DArray)
                                 );
                                 let key = BatchKey::new(
                                     kind,
                                     non_segmented_blend_mode,
                                     BatchTextures::render_target_cache(),
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -198,17 +198,21 @@ impl<'a> DisplayListFlattener<'a> {
             root_pic_index: PictureIndex(0),
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
-        flattener.flatten_root(root_pipeline, &root_pipeline.viewport_size);
+
+        flattener.flatten_root(
+            root_pipeline,
+            &root_pipeline.viewport_size,
+        );
 
         debug_assert!(flattener.sc_stack.is_empty());
 
         new_scene.root_pipeline_id = Some(root_pipeline_id);
         new_scene.pipeline_epochs = scene.pipeline_epochs.clone();
         new_scene.pipelines = scene.pipelines.clone();
 
         FrameBuilder::with_display_list_flattener(
@@ -236,17 +240,21 @@ impl<'a> DisplayListFlattener<'a> {
         pipeline_id: PipelineId,
         items: ItemRange<ClipId>,
     ) -> impl 'a + Iterator<Item = ClipId> {
         self.scene
             .get_display_list_for_pipeline(pipeline_id)
             .get(items)
     }
 
-    fn flatten_root(&mut self, pipeline: &'a ScenePipeline, frame_size: &LayoutSize) {
+    fn flatten_root(
+        &mut self,
+        pipeline: &'a ScenePipeline,
+        frame_size: &LayoutSize,
+    ) {
         let pipeline_id = pipeline.pipeline_id;
         let reference_frame_info = self.simple_scroll_and_clip_chain(
             &ClipId::root_reference_frame(pipeline_id),
         );
 
         let root_scroll_node = ClipId::root_scroll_node(pipeline_id);
 
         self.push_stacking_context(
@@ -257,16 +265,18 @@ impl<'a> DisplayListFlattener<'a> {
             true,
             root_scroll_node,
             None,
             RasterSpace::Screen,
         );
 
         // For the root pipeline, there's no need to add a full screen rectangle
         // here, as it's handled by the framebuffer clear.
+        // TODO(gw): In future, we can probably remove this code completely and handle
+        //           it as part of the tile cache background color clearing.
         if self.scene.root_pipeline_id != Some(pipeline_id) {
             if let Some(pipeline) = self.scene.pipelines.get(&pipeline_id) {
                 if let Some(bg_color) = pipeline.background_color {
                     let root_bounds = LayoutRect::new(LayoutPoint::zero(), *frame_size);
                     let info = LayoutPrimitiveInfo::new(root_bounds);
                     self.add_solid_rectangle(
                         reference_frame_info,
                         &info,
@@ -494,17 +504,20 @@ impl<'a> DisplayListFlattener<'a> {
             ClipId::root_reference_frame(iframe_pipeline_id),
             Some(ExternalScrollId(0, iframe_pipeline_id)),
             iframe_pipeline_id,
             &iframe_rect,
             &pipeline.content_size,
             ScrollSensitivity::ScriptAndInputEvents,
         );
 
-        self.flatten_root(pipeline, &iframe_rect.size);
+        self.flatten_root(
+            pipeline,
+            &iframe_rect.size,
+        );
 
         self.pipeline_clip_chain_stack.pop();
     }
 
     fn flatten_item<'b>(
         &'b mut self,
         item: DisplayItemRef<'a, 'b>,
         pipeline_id: PipelineId,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -65,17 +65,17 @@ pub struct FrameBuilder {
     pub hit_testing_runs: Vec<HitTestingRun>,
     pub config: FrameBuilderConfig,
 }
 
 pub struct FrameBuildingContext<'a> {
     pub device_pixel_scale: DevicePixelScale,
     pub scene_properties: &'a SceneProperties,
     pub pipelines: &'a FastHashMap<PipelineId, Arc<ScenePipeline>>,
-    pub world_rect: WorldRect,
+    pub screen_world_rect: WorldRect,
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub max_local_clip: LayoutRect,
 }
 
 pub struct FrameBuildingState<'a> {
     pub render_tasks: &'a mut RenderTaskTree,
     pub profile_counters: &'a mut FrameProfileCounters,
     pub clip_store: &'a mut ClipStore,
@@ -90,24 +90,24 @@ pub struct FrameBuildingState<'a> {
 }
 
 /// Immutable context of a picture when processing children.
 #[derive(Debug)]
 pub struct PictureContext {
     pub pic_index: PictureIndex,
     pub pipeline_id: PipelineId,
     pub apply_local_clip_rect: bool,
-    pub inflation_factor: f32,
     pub allow_subpixel_aa: bool,
     pub is_passthrough: bool,
     pub raster_space: RasterSpace,
     pub surface_spatial_node_index: SpatialNodeIndex,
     pub raster_spatial_node_index: SpatialNodeIndex,
     /// The surface that this picture will render on.
     pub surface_index: SurfaceIndex,
+    pub dirty_world_rect: WorldRect,
 }
 
 /// Mutable state of a picture that gets modified when
 /// the children are processed.
 pub struct PictureState {
     pub is_cacheable: bool,
     pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
     pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
@@ -198,36 +198,37 @@ impl FrameBuilder {
         }
 
         scratch.begin_frame();
 
         let root_spatial_node_index = clip_scroll_tree.root_reference_frame_index();
 
         const MAX_CLIP_COORD: f32 = 1.0e9;
 
-        let world_rect = (self.screen_rect.to_f32() / device_pixel_scale).round_out();
+        let screen_world_rect = (self.screen_rect.to_f32() / device_pixel_scale).round_out();
 
         let frame_context = FrameBuildingContext {
             device_pixel_scale,
             scene_properties,
             pipelines,
-            world_rect,
+            screen_world_rect,
             clip_scroll_tree,
             max_local_clip: LayoutRect::new(
                 LayoutPoint::new(-MAX_CLIP_COORD, -MAX_CLIP_COORD),
                 LayoutSize::new(2.0 * MAX_CLIP_COORD, 2.0 * MAX_CLIP_COORD),
             ),
         };
 
         // Construct a dummy root surface, that represents the
         // main framebuffer surface.
         let root_surface = SurfaceInfo::new(
             ROOT_SPATIAL_NODE_INDEX,
             ROOT_SPATIAL_NODE_INDEX,
-            world_rect,
+            0.0,
+            screen_world_rect,
             clip_scroll_tree,
         );
         surfaces.push(root_surface);
 
         let mut pic_update_state = PictureUpdateState::new(surfaces);
 
         // The first major pass of building a frame is to walk the picture
         // tree. This pass must be quick (it should never touch individual
@@ -235,16 +236,19 @@ impl FrameBuilder {
         // will create surfaces. In the future, this will be expanded to
         // set up render tasks, determine scaling of surfaces, and detect
         // which surfaces have valid cached surfaces that don't need to
         // be rendered this frame.
         self.prim_store.update_picture(
             self.root_pic_index,
             &mut pic_update_state,
             &frame_context,
+            resource_cache,
+            &resources.prim_data_store,
+            &self.clip_store,
         );
 
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
@@ -293,16 +297,18 @@ impl FrameBuilder {
         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(),
         );
 
         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));
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,35 +1,41 @@
 /* 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::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel};
-use api::{DevicePixelScale, RasterRect, RasterSpace};
-use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect};
+use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
+use api::{DevicePixelScale, RasterRect, RasterSpace, PictureSize, DeviceIntPoint, ColorF, ImageKey};
+use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor};
 use box_shadow::{BLUR_SAMPLE_SCALE};
-use clip::{ClipNodeCollector, ClipStore};
+use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
-use euclid::{TypedScale, vec3, TypedRect};
+use device::TextureFilter;
+use euclid::{TypedScale, vec3, TypedRect, TypedPoint2D, TypedSize2D};
+use euclid::approxeq::ApproxEq;
 use internal_types::{FastHashMap, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use gpu_cache::{GpuCacheAddress, GpuCacheHandle};
 use gpu_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, PrimitiveDataInterner};
-use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle};
+use prim_store::{get_raster_rects, PrimitiveDataInterner, PrimitiveDataStore, CoordinateSpaceMapping};
+use prim_store::{PrimitiveDetails, BrushKind, Primitive, OpacityBindingStorage};
+use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
+use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
 use smallvec::SmallVec;
-use surface::SurfaceDescriptor;
+use surface::{SurfaceDescriptor, TransformKey};
 use std::{mem, ops};
+use texture_cache::{Eviction, TextureCacheHandle};
 use tiling::RenderTargetKind;
-use util::{TransformedRectKind, MatrixHelpers, MaxRect};
+use util::{TransformedRectKind, MatrixHelpers, MaxRect, RectHelpers};
 
 /*
  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
@@ -38,30 +44,602 @@ 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,
 }
 
+/// 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>;
+
+/// The size in device pixels of a cached tile. The currently chosen
+/// size is arbitrary. We should do some profiling to find the best
+/// size for real world pages.
+pub const TILE_SIZE_DP: i32 = 512;
+
+/// Information about the state of a transform dependency.
+#[derive(Debug)]
+pub struct TransformInfo {
+    /// Quantized transform value
+    key: TransformKey,
+    /// Tiles check this to see if the dependencies have changed.
+    changed: bool,
+}
+
+/// Information about a cached tile.
+#[derive(Debug)]
+pub struct Tile {
+    // TODO(gw): We could perhaps use a bitset here instead of a hash set?
+    /// The set of transform values that primitives in this tile depend on.
+    transforms: FastHashSet<SpatialNodeIndex>,
+    /// The set of opacity bindings that this tile depends on.
+    opacity_bindings: FastHashMap<PropertyBindingId, f32>,
+    /// Set of image keys that this tile depends on.
+    image_keys: FastHashSet<ImageKey>,
+    /// If true, this tile is marked valid, and the existing texture
+    /// cache handle can be used. Tiles are invalidated during the
+    /// build_dirty_regions method.
+    is_valid: bool,
+    /// If false, this tile cannot be cached (e.g. it has an external
+    /// video image attached to it). In future, we could add an API
+    /// for the embedder to tell us if the external image changed.
+    /// This is set during primitive dependency updating.
+    is_cacheable: bool,
+    /// If true, this tile is currently in use by the cache. It
+    /// may be false if the tile is outside the bounding rect of
+    /// the current picture, but hasn't been discarded yet. This
+    /// is calculated during primitive dependency updating.
+    in_use: bool,
+    /// 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,
+}
+
+impl Tile {
+    /// Construct a new, invalid tile.
+    fn new() -> Self {
+        Tile {
+            transforms: FastHashSet::default(),
+            opacity_bindings: FastHashMap::default(),
+            image_keys: FastHashSet::default(),
+            is_valid: false,
+            is_visible: false,
+            is_cacheable: true,
+            in_use: false,
+            handle: TextureCacheHandle::invalid(),
+        }
+    }
+}
+
+/// 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: PictureRect,
+    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: PictureSize,
+    /// 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,
+    /// List of transform keys - used to check if transforms
+    /// have changed.
+    pub transforms: Vec<TransformInfo>,
+    /// A helper struct to map local rects into picture coords.
+    pub space_mapper: SpaceMapper<LayoutPixel, PicturePixel>,
+    /// If true, we need to update the prim dependencies, due
+    /// to relative transforms changing. The dependencies are
+    /// stored in each tile, and are a list of things that
+    /// force the tile to re-rasterize if they change (e.g.
+    /// images, transforms).
+    pub needs_update: bool,
+    /// If Some(..) the region that is dirty in this picture.
+    pub dirty_region: Option<DirtyRegion>,
+}
+
+impl TileCache {
+    /// Construct a new tile cache.
+    pub fn new() -> Self {
+        TileCache {
+            tiles: Vec::new(),
+            old_tiles: FastHashMap::default(),
+            tile_rect: TileRect::zero(),
+            local_tile_size: PictureSize::zero(),
+            transforms: Vec::new(),
+            needs_update: true,
+            dirty_region: None,
+            space_mapper: SpaceMapper::new(
+                ROOT_SPATIAL_NODE_INDEX,
+                PictureRect::zero(),
+            ),
+        }
+    }
+
+    /// Update the transforms array for this tile cache from the clip-scroll
+    /// tree. This marks each transform as changed for later use during
+    /// tile invalidation.
+    pub fn update_transforms(
+        &mut self,
+        surface_spatial_node_index: SpatialNodeIndex,
+        frame_context: &FrameBuildingContext,
+    ) {
+        // Initialize the space mapper with current bounds,
+        // which is used during primitive dependency updates.
+        let world_mapper = SpaceMapper::new_with_target(
+            ROOT_SPATIAL_NODE_INDEX,
+            surface_spatial_node_index,
+            frame_context.screen_world_rect,
+            frame_context.clip_scroll_tree,
+        );
+
+        let pic_bounds = world_mapper
+            .unmap(&frame_context.screen_world_rect)
+            .unwrap_or(PictureRect::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_DP as f32 / frame_context.device_pixel_scale.0,
+            TILE_SIZE_DP 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;
+
+        // Walk the transforms and see if we need to rebuild the primitive
+        // dependencies for each tile.
+        // TODO(gw): We could be smarter here and only rebuild for the primitives
+        //           which are affected by transforms that have changed.
+        self.needs_update = if self.transforms.len() == frame_context.clip_scroll_tree.spatial_nodes.len() {
+            // If the transform array length is the same, then we can walk the list
+            // and check if the values of each transform are the same.
+            let mut any_transforms_changed = false;
+
+            for (i, transform) in self.transforms.iter_mut().enumerate() {
+                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
+                    surface_spatial_node_index,
+                    SpatialNodeIndex(i),
+                    frame_context.clip_scroll_tree,
+                ).expect("todo: handle invalid mappings");
+
+                let key = mapping.into();
+                transform.changed = transform.key != key;
+                transform.key = key;
+
+                any_transforms_changed |= transform.changed;
+            }
+
+            any_transforms_changed
+        } else {
+            // If the size of the transforms array changed, just invalidate all the transforms for now.
+            self.transforms.clear();
+
+            for i in 0 .. frame_context.clip_scroll_tree.spatial_nodes.len() {
+                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
+                    surface_spatial_node_index,
+                    SpatialNodeIndex(i),
+                    frame_context.clip_scroll_tree,
+                ).expect("todo: handle invalid mappings");
+
+                self.transforms.push(TransformInfo {
+                    key: mapping.into(),
+                    changed: true,
+                });
+            }
+
+            true
+        };
+
+        // If we need to update the dependencies for tiles, walk each tile
+        // and clear the transforms and opacity bindings arrays.
+        if self.needs_update {
+            debug_assert!(self.old_tiles.is_empty());
+
+            // Clear any dependencies on the set of existing tiles, since
+            // they are going to be rebuilt. Drain the tiles list and add
+            // them to the old_tiles hash, for re-use next frame build.
+            for (i, mut tile) in self.tiles.drain(..).enumerate() {
+                let y = i as i32 / self.tile_rect.size.width;
+                let x = i as i32 % self.tile_rect.size.width;
+                tile.transforms.clear();
+                tile.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();
+        }
+    }
+
+    /// 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;
+
+        if self.tile_rect.size.width == x_tiles && self.tile_rect.size.height == y_tiles {
+            return;
+        }
+
+        // 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 = 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())
+                } else {
+                    self.old_tiles.remove(&TileOffset::new(x + x0, y + y0)).unwrap_or_else(Tile::new)
+                };
+                new_tiles.push(tile);
+            }
+        }
+
+        self.tiles = new_tiles;
+        self.tile_rect.origin = TileOffset::new(x0, y0);
+        self.tile_rect.size = TileSize::new(x_tiles, y_tiles);
+    }
+
+    /// Update the dependencies for each tile for a given primitive instance.
+    pub fn update_prim_dependencies(
+        &mut self,
+        prim_instance: &PrimitiveInstance,
+        surface_spatial_node_index: SpatialNodeIndex,
+        clip_scroll_tree: &ClipScrollTree,
+        prim_data_store: &PrimitiveDataStore,
+        clip_chain_nodes: &[ClipChainNode],
+        primitives: &[Primitive],
+        pictures: &[PicturePrimitive],
+        resource_cache: &ResourceCache,
+        scene_properties: &SceneProperties,
+        opacity_binding_store: &OpacityBindingStorage,
+    ) {
+        self.space_mapper.set_target_spatial_node(
+            prim_instance.spatial_node_index,
+            clip_scroll_tree,
+        );
+
+        let prim_data = &prim_data_store[prim_instance.prim_data_handle];
+
+        // Map the primitive local rect into the picture space.
+        let rect = match self.space_mapper.map(&prim_data.prim_rect) {
+            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 {
+            return;
+        }
+
+        // Get the tile coordinates in the picture space.
+        let x0 = (rect.origin.x / self.local_tile_size.width).floor() as i32;
+        let y0 = (rect.origin.y / self.local_tile_size.height).floor() as i32;
+        let x1 = ((rect.origin.x + rect.size.width) / self.local_tile_size.width).ceil() as i32;
+        let y1 = ((rect.origin.y + rect.size.height) / self.local_tile_size.height).ceil() as i32;
+
+        // Update the tile array allocation if needed.
+        self.reconfigure_tiles_if_required(x0, y0, x1, y1);
+
+        // Build the list of resources that this primitive has dependencies on.
+        let mut is_cacheable = true;
+        let mut opacity_bindings: SmallVec<[(PropertyBindingId, f32); 4]> = SmallVec::new();
+        let mut clip_chain_spatial_nodes: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
+        let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
+        let mut current_clip_chain_id = prim_instance.clip_chain_id;
+
+        match prim_instance.kind {
+            PrimitiveInstanceKind::Picture { pic_index } => {
+                // Pictures can depend on animated opacity bindings.
+                let pic = &pictures[pic_index.0];
+                if let Some(PictureCompositeMode::Filter(FilterOp::Opacity(binding, _))) = pic.requested_composite_mode {
+                    if let PropertyBinding::Binding(key, default) = binding {
+                        opacity_bindings.push((key.id, default));
+                    }
+                }
+            }
+            PrimitiveInstanceKind::LegacyPrimitive { prim_index } => {
+                let prim = &primitives[prim_index.0];
+
+                // Some primitives can not be cached (e.g. external video images)
+                is_cacheable = prim_instance.is_cacheable(
+                    &prim.details,
+                    resource_cache,
+                );
+
+                match prim.details {
+                    PrimitiveDetails::Brush(ref brush) => {
+                        match brush.kind {
+                            // Rectangles and images may depend on opacity bindings.
+                            // TODO(gw): In future, we might be able to completely remove
+                            //           opacity collapsing support. It's of limited use
+                            //           once we have full picture caching.
+                            BrushKind::Solid { opacity_binding_index, .. } => {
+                                let opacity_binding = &opacity_binding_store[opacity_binding_index];
+                                for binding in &opacity_binding.bindings {
+                                    if let PropertyBinding::Binding(key, default) = binding {
+                                        opacity_bindings.push((key.id, *default));
+                                    }
+                                }
+                            }
+                            BrushKind::Image { opacity_binding_index, ref request, .. } => {
+                                let opacity_binding = &opacity_binding_store[opacity_binding_index];
+                                for binding in &opacity_binding.bindings {
+                                    if let PropertyBinding::Binding(key, default) = binding {
+                                        opacity_bindings.push((key.id, *default));
+                                    }
+                                }
+
+                                image_keys.push(request.key);
+                            }
+                            BrushKind::YuvImage { ref yuv_key, .. } => {
+                                image_keys.extend_from_slice(yuv_key);
+                            }
+                            BrushKind::RadialGradient { .. } |
+                            BrushKind::LinearGradient { .. } |
+                            BrushKind::Border { .. } => {
+                            }
+                        }
+                    }
+                }
+            }
+            PrimitiveInstanceKind::TextRun { .. } |
+            PrimitiveInstanceKind::LineDecoration { .. } |
+            PrimitiveInstanceKind::Clear => {
+                // These don't contribute dependencies
+            }
+        }
+
+        for (key, current) in &mut opacity_bindings {
+            if let Some(value) = scene_properties.get_float_value(*key) {
+                *current = value;
+            }
+        }
+
+        // The transforms of any clips that are relative to the picture may affect
+        // the content rendered by this primitive.
+        while current_clip_chain_id != ClipChainId::NONE {
+            let clip_chain_node = &clip_chain_nodes[current_clip_chain_id.0 as usize];
+            // We only care about clip nodes that have transforms that are children
+            // of the surface, since clips that are positioned by parents will be
+            // handled by the clip collector when these tiles are composited.
+            if clip_chain_node.spatial_node_index > surface_spatial_node_index {
+                clip_chain_spatial_nodes.push(clip_chain_node.spatial_node_index);
+            }
+            current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+        }
+
+        // Normalize the tile coordinates before adding to tile dependencies.
+        // For each affected tile, mark any of the primitive dependencies.
+        for y in y0 - self.tile_rect.origin.y .. y1 - self.tile_rect.origin.y {
+            for x in x0 - self.tile_rect.origin.x .. x1 - self.tile_rect.origin.x {
+                let index = (y * self.tile_rect.size.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;
+
+                // Include any image keys this tile depends on.
+                for image_key in &image_keys {
+                    tile.image_keys.insert(*image_key);
+                }
+
+                // Include the transform of the primitive itself.
+                tile.transforms.insert(prim_instance.spatial_node_index);
+
+                // Include the transforms of any relevant clip nodes for this primitive.
+                for clip_chain_spatial_node in &clip_chain_spatial_nodes {
+                    tile.transforms.insert(*clip_chain_spatial_node);
+                }
+
+                // Include any opacity bindings this primitive depends on.
+                for &(id, value) in &opacity_bindings {
+                    tile.opacity_bindings.insert(id, value);
+                }
+            }
+        }
+    }
+
+    /// Build the dirty region(s) for the tile cache after all primitive
+    /// dependencies have been updated.
+    pub fn build_dirty_regions(
+        &mut self,
+        surface_spatial_node_index: SpatialNodeIndex,
+        frame_context: &FrameBuildingContext,
+        resource_cache: &mut ResourceCache,
+    ) {
+        self.needs_update = false;
+
+        for (_, tile) in self.old_tiles.drain() {
+            if resource_cache.texture_cache.is_allocated(&tile.handle) {
+                resource_cache.texture_cache.mark_unused(&tile.handle);
+            }
+        }
+
+        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 mut tile_offset = DeviceIntPoint::new(
+            self.tile_rect.size.width,
+            self.tile_rect.size.height,
+        );
+
+        let mut dirty_rect = PictureRect::zero();
+
+        // Step through each tile and invalidate if the dependencies have changed.
+        for y in 0 .. self.tile_rect.size.height {
+            for x in 0 .. self.tile_rect.size.width {
+                let i = y * self.tile_rect.size.width + x;
+                let tile = &mut self.tiles[i as usize];
+
+                let tile_rect = PictureRect::new(
+                    PicturePoint::new(
+                        (self.tile_rect.origin.x + x) as f32 * self.local_tile_size.width,
+                        (self.tile_rect.origin.y + y) as f32 * self.local_tile_size.height,
+                    ),
+                    self.local_tile_size,
+                );
+
+                // Invalidate the tile if not cacheable
+                if !tile.is_cacheable {
+                    tile.is_valid = false;
+                }
+
+                if !tile.in_use {
+                    tile.is_valid = false;
+                }
+
+                // Invalidate the tile if any images have changed
+                for image_key in &tile.image_keys {
+                    if resource_cache.is_image_dirty(*image_key) {
+                        tile.is_valid = false;
+                        break;
+                    }
+                }
+
+                // Invalidate the tile if any dependent transforms changed
+                for node_index in &tile.transforms {
+                    if self.transforms[node_index.0].changed {
+                        tile.is_valid = false;
+                        break;
+                    }
+                }
+
+                // Invalidate the tile if any opacity bindings changed.
+                for (id, old) in &mut tile.opacity_bindings {
+                    if let Some(new) = frame_context.scene_properties.get_float_value(*id) {
+                        if !new.approx_eq(old) {
+                            tile.is_valid = false;
+                            break;
+                        }
+                    }
+                }
+
+                // Invalidate the tile if it was evicted by the texture cache.
+                if !resource_cache.texture_cache.is_allocated(&tile.handle) {
+                    tile.is_valid = false;
+                }
+
+                // Check if this tile is actually visible.
+                let tile_world_rect = world_mapper
+                    .map(&tile_rect)
+                    .expect("bug: unable to map tile to world coords");
+                tile.is_visible = frame_context.screen_world_rect.intersects(&tile_world_rect);
+
+                // If we have an invalid tile, which is also visible, add it to the
+                // dirty rect we will need to draw.
+                if !tile.is_valid && tile.is_visible {
+                    dirty_rect = dirty_rect.union(&tile_rect);
+                    tile_offset.x = tile_offset.x.min(x);
+                    tile_offset.y = tile_offset.y.min(y);
+                }
+            }
+        }
+
+        self.dirty_region = if dirty_rect.is_empty() {
+            None
+        } else {
+            let dirty_world_rect = world_mapper.map(&dirty_rect).expect("todo");
+            Some(DirtyRegion {
+                dirty_rect,
+                tile_offset,
+                dirty_world_rect,
+            })
+        };
+    }
+}
+
 /// Maintains a stack of picture and surface information, that
 /// is used during the initial picture traversal.
 pub struct PictureUpdateState<'a> {
     pub surfaces: &'a mut Vec<SurfaceInfo>,
     surface_stack: Vec<SurfaceIndex>,
     picture_stack: Vec<PictureInfo>,
+    /// A stack of currently active tile caches during traversal.
+    tile_cache_stack: Vec<TileCache>,
+    /// A ref count of how many tile caches on the stack actually
+    /// need to have their primitive dependencies updated.
+    tile_cache_update_count: usize,
 }
 
 impl<'a> PictureUpdateState<'a> {
     pub fn new(surfaces: &'a mut Vec<SurfaceInfo>) -> Self {
         PictureUpdateState {
             surfaces,
             surface_stack: vec![SurfaceIndex(0)],
             picture_stack: Vec::new(),
+            tile_cache_stack: Vec::new(),
+            tile_cache_update_count: 0,
         }
     }
 
     /// Return the current surface
     fn current_surface(&self) -> &SurfaceInfo {
         &self.surfaces[self.surface_stack.last().unwrap().0]
     }
 
@@ -102,16 +680,40 @@ impl<'a> PictureUpdateState<'a> {
     }
 
     /// Pop the picture info off, on the way up the picture traversal
     fn pop_picture(
         &mut self,
     ) -> PictureInfo {
         self.picture_stack.pop().unwrap()
     }
+
+    /// Push a tile cache onto the traversal state.
+    pub fn push_tile_cache(
+        &mut self,
+        tile_cache: TileCache,
+    ) {
+        if tile_cache.needs_update {
+            self.tile_cache_update_count += 1;
+        }
+        self.tile_cache_stack.push(tile_cache);
+    }
+
+    /// Pop a tile cache from the traversal state.
+    pub fn pop_tile_cache(
+        &mut self,
+    ) -> TileCache {
+        let tile_cache = self.tile_cache_stack.pop().unwrap();
+
+        if tile_cache.needs_update {
+            self.tile_cache_update_count -= 1;
+        }
+
+        tile_cache
+    }
 }
 
 #[derive(Debug, Copy, Clone)]
 pub struct SurfaceIndex(pub usize);
 
 pub const ROOT_SURFACE_INDEX: SurfaceIndex = SurfaceIndex(0);
 
 /// Information about an offscreen surface. For now,
@@ -123,31 +725,33 @@ pub const ROOT_SURFACE_INDEX: SurfaceInd
 #[derive(Debug)]
 pub struct SurfaceInfo {
     /// A local rect defining the size of this surface, in the
     /// coordinate system of the surface itself.
     pub rect: PictureRect,
     /// Helper structs for mapping local rects in different
     /// coordinate systems into the surface coordinates.
     pub map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
-    pub map_surface_to_world: SpaceMapper<PicturePixel, WorldPixel>,
     /// Defines the positioning node for the surface itself,
     /// and the rasterization root for this surface.
     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,
 }
 
 impl SurfaceInfo {
     pub fn new(
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
+        inflation_factor: f32,
         world_rect: WorldRect,
         clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
         let map_surface_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
             world_rect,
             clip_scroll_tree,
@@ -159,22 +763,22 @@ impl SurfaceInfo {
 
         let map_local_to_surface = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
         );
 
         SurfaceInfo {
             rect: PictureRect::zero(),
-            map_surface_to_world,
             map_local_to_surface,
             surface: None,
             raster_spatial_node_index,
             surface_spatial_node_index,
             tasks: Vec::new(),
+            inflation_factor,
         }
     }
 
     /// 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())
     }
@@ -187,25 +791,30 @@ pub struct RasterConfig {
     pub composite_mode: PictureCompositeMode,
     /// Index to the surface descriptor for this
     /// picture.
     pub surface_index: SurfaceIndex,
 }
 
 /// Specifies how this Picture should be composited
 /// onto the target it belongs to.
+#[allow(dead_code)]
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub enum PictureCompositeMode {
     /// Apply CSS mix-blend-mode effect.
     MixBlend(MixBlendMode),
     /// Apply a CSS filter.
     Filter(FilterOp),
     /// Draw to intermediate surface, copy straight across. This
     /// is used for CSS isolation, and plane splitting.
     Blit,
+    /// Used to cache a picture as a series of tiles.
+    TileCache {
+        clear_color: ColorF,
+    },
 }
 
 // Stores the location of the picture if it is drawn to
 // an intermediate surface. This can be a render task if
 // it is not persisted, or a texture cache item if the
 // picture is cached in the texture cache.
 #[derive(Debug)]
 pub enum PictureSurface {
@@ -445,29 +1054,32 @@ pub struct PicturePrimitive {
     pub frame_output_pipeline_id: Option<PipelineId>,
     // An optional cache handle for storing extra data
     // in the GPU cache, depending on the type of
     // picture.
     pub extra_gpu_data_handle: GpuCacheHandle,
 
     /// The spatial node index of this picture when it is
     /// composited into the parent picture.
-    spatial_node_index: SpatialNodeIndex,
+    pub spatial_node_index: SpatialNodeIndex,
 
     /// The local rect of this picture. It is built
     /// dynamically during the first picture traversal.
     pub local_rect: LayoutRect,
 
     /// Local clip rect for this picture.
     pub local_clip_rect: LayoutRect,
 
     /// A descriptor for this surface that can be used as a cache key.
     surface_desc: Option<SurfaceDescriptor>,
 
     pub gpu_location: GpuCacheHandle,
+
+    /// If Some(..) the tile cache that is associated with this picture.
+    pub tile_cache: Option<TileCache>,
 }
 
 impl PicturePrimitive {
     fn resolve_scene_properties(&mut self, properties: &SceneProperties) -> bool {
         match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(ref mut filter)) => {
                 match *filter {
                     FilterOp::Opacity(ref binding, ref mut value) => {
@@ -520,16 +1132,25 @@ impl PicturePrimitive {
                 &prim_list.prim_instances,
                 spatial_node_index,
                 clip_store,
             )
         } else {
             None
         };
 
+        let tile_cache = match requested_composite_mode {
+            Some(PictureCompositeMode::TileCache { .. }) => {
+                Some(TileCache::new())
+            }
+            Some(_) | None => {
+                None
+            }
+        };
+
         PicturePrimitive {
             surface_desc,
             prim_list,
             state: None,
             secondary_render_task_id: None,
             requested_composite_mode,
             raster_config: None,
             context_3d,
@@ -537,16 +1158,17 @@ impl PicturePrimitive {
             extra_gpu_data_handle: GpuCacheHandle::new(),
             apply_local_clip_rect,
             pipeline_id,
             requested_raster_space,
             spatial_node_index,
             local_rect: LayoutRect::zero(),
             local_clip_rect,
             gpu_location: GpuCacheHandle::new(),
+            tile_cache,
         }
     }
 
     pub fn take_context(
         &mut self,
         pic_index: PictureIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
@@ -554,16 +1176,36 @@ impl PicturePrimitive {
         parent_allows_subpixel_aa: bool,
         frame_state: &mut FrameBuildingState,
         frame_context: &FrameBuildingContext,
     ) -> 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) => {
+                // If a tile cache is present, extract the dirty
+                // world rect from the dirty region. If there is
+                // no dirty region there is nothing to render.
+                // TODO(gw): We could early out here in that case?
+                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
+            }
+        };
+
         // 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) => {
                 let surface = &frame_state.surfaces[raster_config.surface_index.0];
 
                 (surface.raster_spatial_node_index, self.spatial_node_index, raster_config.surface_index)
@@ -576,32 +1218,33 @@ impl PicturePrimitive {
         if self.raster_config.is_some() {
             frame_state.clip_store
                 .push_surface(surface_spatial_node_index);
         }
 
         let map_pic_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
-            frame_context.world_rect,
+            dirty_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
-            frame_context,
+            dirty_world_rect,
+            frame_context.clip_scroll_tree,
         );
 
         let plane_splitter = match self.context_3d {
             Picture3DContext::Out => {
                 None
             }
             Picture3DContext::In { root_data: Some(_), .. } => {
                 Some(PlaneSplitter::new())
@@ -617,41 +1260,43 @@ impl PicturePrimitive {
             map_pic_to_world,
             map_pic_to_raster,
             map_raster_to_world,
             plane_splitter,
         };
 
         // Disallow subpixel AA if an intermediate surface is needed.
         // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
-        let allow_subpixel_aa = parent_allows_subpixel_aa &&
-            self.raster_config.is_none();
-
-        let inflation_factor = match self.raster_config {
-            Some(RasterConfig { composite_mode: PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)), .. }) => {
-                // The amount of extra space needed for primitives inside
-                // this picture to ensure the visibility check is correct.
-                BLUR_SAMPLE_SCALE * blur_radius
+        let allow_subpixel_aa = match self.raster_config {
+            Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { clear_color, .. }, .. }) => {
+                // If the tile cache has an opaque background, then it's fine to use
+                // subpixel rendering (this is the common case).
+                clear_color.a >= 1.0
+            },
+            Some(_) => {
+                false
             }
-            _ => {
-                0.0
+            None => {
+                true
             }
         };
+        // Still disable subpixel AA if parent forbids it
+        let allow_subpixel_aa = parent_allows_subpixel_aa && allow_subpixel_aa;
 
         let context = PictureContext {
             pic_index,
             pipeline_id: self.pipeline_id,
             apply_local_clip_rect: self.apply_local_clip_rect,
-            inflation_factor,
             allow_subpixel_aa,
             is_passthrough: self.raster_config.is_none(),
             raster_space: self.requested_raster_space,
             raster_spatial_node_index,
             surface_spatial_node_index,
             surface_index,
+            dirty_world_rect,
         };
 
         let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
 
         Some((context, state, prim_list))
     }
 
     pub fn restore_context(
@@ -816,16 +1461,28 @@ impl PicturePrimitive {
         self.raster_config = None;
 
         // Resolve animation properties, and early out if the filter
         // properties make this picture invisible.
         if !self.resolve_scene_properties(frame_context.scene_properties) {
             return None;
         }
 
+        // If we have a tile cache for this picture, see if any of the
+        // relative transforms have changed, which means we need to
+        // re-map the dependencies of any child primitives.
+        if let Some(mut tile_cache) = self.tile_cache.take() {
+            tile_cache.update_transforms(
+                self.spatial_node_index,
+                frame_context,
+            );
+
+            state.push_tile_cache(tile_cache);
+        }
+
         // Push information about this pic on stack for children to read.
         state.push_picture(PictureInfo {
             spatial_node_index: self.spatial_node_index,
         });
 
         // See if this picture actually needs a surface for compositing.
         let actual_composite_mode = match self.requested_composite_mode {
             Some(PictureCompositeMode::Filter(filter)) if filter.is_noop() => None,
@@ -864,21 +1521,33 @@ impl PicturePrimitive {
             let raster_space = RasterSpace::Screen;
 
             let raster_spatial_node_index = if establishes_raster_root {
                 surface_spatial_node_index
             } else {
                 parent_raster_spatial_node_index
             };
 
+            let inflation_factor = match composite_mode {
+                PictureCompositeMode::Filter(FilterOp::Blur(blur_radius)) => {
+                    // The amount of extra space needed for primitives inside
+                    // this picture to ensure the visibility check is correct.
+                    BLUR_SAMPLE_SCALE * blur_radius
+                }
+                _ => {
+                    0.0
+                }
+            };
+
             let surface_index = state.push_surface(
                 SurfaceInfo::new(
                     surface_spatial_node_index,
                     raster_spatial_node_index,
-                    frame_context.world_rect,
+                    inflation_factor,
+                    frame_context.screen_world_rect,
                     &frame_context.clip_scroll_tree,
                 )
             );
 
             self.raster_config = Some(RasterConfig {
                 composite_mode,
                 surface_index,
             });
@@ -893,23 +1562,59 @@ impl PicturePrimitive {
                     raster_space,
                 );
             }
         }
 
         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,
+        state: &mut PictureUpdateState,
+        frame_context: &FrameBuildingContext,
+        resource_cache: &mut ResourceCache,
+        prim_data_store: &PrimitiveDataStore,
+        primitives: &[Primitive],
+        pictures: &[PicturePrimitive],
+        clip_store: &ClipStore,
+        opacity_binding_store: &OpacityBindingStorage,
+    ) {
+        if state.tile_cache_update_count == 0 {
+            return;
+        }
+
+        for prim_instance in &self.prim_list.prim_instances {
+            for tile_cache in &mut state.tile_cache_stack {
+                tile_cache.update_prim_dependencies(
+                    prim_instance,
+                    self.spatial_node_index,
+                    &frame_context.clip_scroll_tree,
+                    prim_data_store,
+                    &clip_store.clip_chain_nodes,
+                    primitives,
+                    pictures,
+                    resource_cache,
+                    frame_context.scene_properties,
+                    opacity_binding_store,
+                );
+            }
+        }
+    }
+
     /// Called after updating child pictures during the initial
     /// picture traversal.
     pub fn post_update(
         &mut self,
         child_pictures: PictureList,
         state: &mut PictureUpdateState,
         frame_context: &FrameBuildingContext,
+        resource_cache: &mut ResourceCache,
     ) {
         // Pop the state information about this picture.
         state.pop_picture();
 
         for cluster in &mut self.prim_list.clusters {
             // Skip the cluster if backface culled.
             if !cluster.is_backface_visible {
                 let containing_block_index = match self.context_3d {
@@ -981,23 +1686,45 @@ impl PicturePrimitive {
         }
 
         // Restore the pictures list used during recursion.
         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 self.raster_config.is_some() {
+        if let Some(ref raster_config) = self.raster_config {
             let surface_rect = state.current_surface().rect;
-            let surface_rect = TypedRect::from_untyped(&surface_rect.to_untyped());
+
+            if let PictureCompositeMode::TileCache { .. } = raster_config.composite_mode {
+                let mut tile_cache = state.pop_tile_cache();
+
+                // Build the dirty region(s) for this tile cache.
+                tile_cache.build_dirty_regions(
+                    self.spatial_node_index,
+                    frame_context,
+                    resource_cache,
+                );
+
+                self.tile_cache = Some(tile_cache);
+            }
+
+            let mut surface_rect = TypedRect::from_untyped(&surface_rect.to_untyped());
 
             // Pop this surface from the stack
             state.pop_surface();
 
+            // Drop shadows draw both a content and shadow rect, so need to expand the local
+            // rect of any surfaces to be composited in parent surfaces correctly.
+            if let PictureCompositeMode::Filter(FilterOp::DropShadow(offset, ..)) = raster_config.composite_mode {
+                let content_rect = surface_rect;
+                let shadow_rect = surface_rect.translate(&offset);
+                surface_rect = content_rect.union(&shadow_rect);
+            }
+
             // Propagate up to parent surface, now that we know this surface's static rect
             let parent_surface = state.current_surface_mut();
             parent_surface.map_local_to_surface.set_target_spatial_node(
                 self.spatial_node_index,
                 frame_context.clip_scroll_tree,
             );
             if let Some(parent_surface_rect) = parent_surface
                 .map_local_to_surface
@@ -1038,17 +1765,18 @@ impl PicturePrimitive {
             let surface_info = &mut frame_state.surfaces[raster_config.surface_index.0];
             (surface_info.raster_spatial_node_index, surface_info.take_render_tasks())
         };
         let surfaces = &mut frame_state.surfaces;
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             prim_instance.spatial_node_index,
             raster_spatial_node_index,
-            frame_context,
+            pic_context.dirty_world_rect,
+            frame_context.clip_scroll_tree,
         );
 
         let pic_rect = PictureRect::from_untyped(&prim_local_rect.to_untyped());
 
         let (clipped, unclipped) = match get_raster_rects(
             pic_rect,
             &map_pic_to_raster,
             &map_raster_to_world,
@@ -1063,16 +1791,131 @@ 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, .. } => {
+                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.
+                        let descriptor = ImageDescriptor::new(
+                            TILE_SIZE_DP,
+                            TILE_SIZE_DP,
+                            ImageFormat::BGRA8,
+                            false,          // TODO(gw): Detect when background color is opaque!
+                            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 {
+                                    // 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],
+                                        None,
+                                        frame_state.gpu_cache,
+                                        None,
+                                        UvRectKind::Rect,
+                                        Eviction::Eager,
+                                    );
+
+                                    let cache_item = frame_state
+                                        .resource_cache
+                                        .get_texture_cache_item(&tile.handle);
+
+                                    // Set up the blit command now that we know where the dest
+                                    // rect is in the texture cache.
+                                    let offset = DeviceIntPoint::new(
+                                        (x - dirty_region.tile_offset.x) * TILE_SIZE_DP,
+                                        (y - dirty_region.tile_offset.y) * TILE_SIZE_DP,
+                                    );
+
+                                    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;
+                    }
+                }
+            }
             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
                 // draw the whole picture, without clipping it to the screen
@@ -1122,16 +1965,18 @@ impl PicturePrimitive {
                     let picture_task = RenderTask::new_picture(
                         RenderTaskLocation::Dynamic(None, device_rect.size),
                         unclipped.size,
                         pic_index,
                         device_rect.origin,
                         child_tasks,
                         uv_rect_kind,
                         pic_context.raster_spatial_node_index,
+                        None,
+                        Vec::new(),
                     );
 
                     let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                     let blur_render_task = RenderTask::new_blur(
                         blur_std_deviation,
                         picture_task_id,
                         frame_state.render_tasks,
@@ -1177,16 +2022,18 @@ impl PicturePrimitive {
                             let picture_task = RenderTask::new_picture(
                                 RenderTaskLocation::Dynamic(None, device_rect.size),
                                 unclipped.size,
                                 pic_index,
                                 device_rect.origin,
                                 child_tasks,
                                 uv_rect_kind,
                                 pic_context.raster_spatial_node_index,
+                                None,
+                                Vec::new(),
                             );
 
                             let picture_task_id = render_tasks.add(picture_task);
 
                             let blur_render_task = RenderTask::new_blur(
                                 blur_std_deviation,
                                 picture_task_id,
                                 render_tasks,
@@ -1232,16 +2079,18 @@ impl PicturePrimitive {
                 let mut picture_task = RenderTask::new_picture(
                     RenderTaskLocation::Dynamic(None, device_rect.size),
                     unclipped.size,
                     pic_index,
                     device_rect.origin,
                     child_tasks,
                     uv_rect_kind,
                     pic_context.raster_spatial_node_index,
+                    None,
+                    Vec::new(),
                 );
                 picture_task.mark_for_saving();
 
                 let picture_task_id = frame_state.render_tasks.add(picture_task);
 
                 let blur_render_task = RenderTask::new_blur(
                     blur_std_deviation.round(),
                     picture_task_id,
@@ -1296,16 +2145,18 @@ impl PicturePrimitive {
                 let picture_task = RenderTask::new_picture(
                     RenderTaskLocation::Dynamic(None, clipped.size),
                     unclipped.size,
                     pic_index,
                     clipped.origin,
                     child_tasks,
                     uv_rect_kind,
                     pic_context.raster_spatial_node_index,
+                    None,
+                    Vec::new(),
                 );
 
                 let readback_task_id = frame_state.render_tasks.add(
                     RenderTask::new_readback(clipped)
                 );
 
                 self.secondary_render_task_id = Some(readback_task_id);
                 surfaces[surface_index.0].tasks.push(readback_task_id);
@@ -1333,16 +2184,18 @@ impl PicturePrimitive {
                 let picture_task = RenderTask::new_picture(
                     RenderTaskLocation::Dynamic(None, clipped.size),
                     unclipped.size,
                     pic_index,
                     clipped.origin,
                     child_tasks,
                     uv_rect_kind,
                     pic_context.raster_spatial_node_index,
+                    None,
+                    Vec::new(),
                 );
 
                 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)
             }
             PictureCompositeMode::Blit => {
                 let uv_rect_kind = calculate_uv_rect_kind(
@@ -1355,16 +2208,18 @@ impl PicturePrimitive {
                 let picture_task = RenderTask::new_picture(
                     RenderTaskLocation::Dynamic(None, clipped.size),
                     unclipped.size,
                     pic_index,
                     clipped.origin,
                     child_tasks,
                     uv_rect_kind,
                     pic_context.raster_spatial_node_index,
+                    None,
+                    Vec::new(),
                 );
 
                 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)
             }
         };
 
@@ -1452,29 +2307,30 @@ fn calculate_uv_rect_kind(
         bottom_left,
         bottom_right,
     }
 }
 
 fn create_raster_mappers(
     surface_spatial_node_index: SpatialNodeIndex,
     raster_spatial_node_index: SpatialNodeIndex,
-    frame_context: &FrameBuildingContext,
+    dirty_world_rect: WorldRect,
+    clip_scroll_tree: &ClipScrollTree,
 ) -> (SpaceMapper<RasterPixel, WorldPixel>, SpaceMapper<PicturePixel, RasterPixel>) {
     let map_raster_to_world = SpaceMapper::new_with_target(
         ROOT_SPATIAL_NODE_INDEX,
         raster_spatial_node_index,
-        frame_context.world_rect,
-        frame_context.clip_scroll_tree,
+        dirty_world_rect,
+        clip_scroll_tree,
     );
 
-    let raster_bounds = map_raster_to_world.unmap(&frame_context.world_rect)
+    let raster_bounds = map_raster_to_world.unmap(&dirty_world_rect)
                                            .unwrap_or(RasterRect::max_rect());
 
     let map_pic_to_raster = SpaceMapper::new_with_target(
         raster_spatial_node_index,
         surface_spatial_node_index,
         raster_bounds,
-        frame_context.clip_scroll_tree,
+        clip_scroll_tree,
     );
 
     (map_raster_to_world, map_pic_to_raster)
 }
--- a/gfx/wr/webrender/src/prim_store.rs
+++ b/gfx/wr/webrender/src/prim_store.rs
@@ -6,16 +6,17 @@ use api::{AlphaType, BorderRadius, Built
 use api::{DeviceIntRect, DeviceIntSize, DevicePixelScale, ExtendMode, DeviceRect};
 use api::{FilterOp, GlyphInstance, GradientStop, ImageKey, ImageRendering, ItemRange, TileOffset};
 use api::{RasterSpace, LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize, LayoutToWorldTransform};
 use api::{LayoutVector2D, PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat, LayoutRectAu};
 use api::{DeviceIntSideOffsets, WorldPixel, BoxShadowClipMode, NormalBorder, WorldRect, LayoutToWorldScale};
 use api::{PicturePixel, RasterPixel, ColorDepth, LineStyle, LineOrientation, LayoutSizeAu, AuHelpers, LayoutVector2DAu};
 use app_units::Au;
 use border::{get_max_scale_for_border, build_border_instances, create_normal_border_prim};
+use clip::ClipStore;
 use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
 use clip::{ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
 use euclid::{TypedTransform3D, TypedRect, TypedScale};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveContext;
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest, ToGpuBlocks};
 use gpu_types::BrushFlags;
@@ -122,40 +123,42 @@ pub enum CoordinateSpaceMapping<F, T> {
     Transform(TypedTransform3D<f32, F, T>),
 }
 
 impl<F, T> CoordinateSpaceMapping<F, T> {
     pub fn new(
         ref_spatial_node_index: SpatialNodeIndex,
         target_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
-    ) -> Self {
+    ) -> Option<Self> {
         let spatial_nodes = &clip_scroll_tree.spatial_nodes;
         let ref_spatial_node = &spatial_nodes[ref_spatial_node_index.0];
         let target_spatial_node = &spatial_nodes[target_node_index.0];
 
         if ref_spatial_node_index == target_node_index {
-            CoordinateSpaceMapping::Local
+            Some(CoordinateSpaceMapping::Local)
         } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-            CoordinateSpaceMapping::ScaleOffset(
+            Some(CoordinateSpaceMapping::ScaleOffset(
                 ref_spatial_node.coordinate_system_relative_scale_offset
                     .inverse()
                     .accumulate(
                         &target_spatial_node.coordinate_system_relative_scale_offset
                     )
-            )
+            ))
         } else {
             let transform = clip_scroll_tree.get_relative_transform(
                 target_node_index,
                 ref_spatial_node_index,
-            ).expect("bug: should have already been culled");
-
-            CoordinateSpaceMapping::Transform(
-                transform.with_source::<F>().with_destination::<T>()
-            )
+            );
+
+            transform.map(|transform| {
+                CoordinateSpaceMapping::Transform(
+                    transform.with_source::<F>().with_destination::<T>()
+                )
+            })
         }
     }
 }
 
 #[derive(Debug)]
 pub struct SpaceMapper<F, T> {
     kind: CoordinateSpaceMapping<F, T>,
     pub ref_spatial_node_index: SpatialNodeIndex,
@@ -194,17 +197,17 @@ impl<F, T> SpaceMapper<F, T> where F: fm
     ) {
         if target_node_index != self.current_target_spatial_node_index {
             self.current_target_spatial_node_index = target_node_index;
 
             self.kind = CoordinateSpaceMapping::new(
                 self.ref_spatial_node_index,
                 target_node_index,
                 clip_scroll_tree,
-            );
+            ).expect("bug: should have been culled by invalid node");
         }
     }
 
     pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 TypedTransform3D::identity()
             }
@@ -605,17 +608,17 @@ pub type PrimitiveDataInterner = intern:
 pub type PrimitiveUid = intern::ItemUid<PrimitiveDataMarker>;
 
 // Maintains a list of opacity bindings that have been collapsed into
 // the color of a single primitive. This is an important optimization
 // that avoids allocating an intermediate surface for most common
 // uses of opacity filters.
 #[derive(Debug)]
 pub struct OpacityBinding {
-    bindings: Vec<PropertyBinding<f32>>,
+    pub bindings: Vec<PropertyBinding<f32>>,
     pub current: f32,
 }
 
 impl OpacityBinding {
     pub fn new() -> OpacityBinding {
         OpacityBinding {
             bindings: Vec::new(),
             current: 1.0,
@@ -2011,33 +2014,51 @@ impl PrimitiveStore {
     /// Update a picture, determining surface configuration,
     /// rasterization roots, and (in future) whether there
     /// are cached surfaces that can be used by this picture.
     pub fn update_picture(
         &mut self,
         pic_index: PictureIndex,
         state: &mut PictureUpdateState,
         frame_context: &FrameBuildingContext,
+        resource_cache: &mut ResourceCache,
+        prim_data_store: &PrimitiveDataStore,
+        clip_store: &ClipStore,
     ) {
         if let Some(children) = self.pictures[pic_index.0].pre_update(
             state,
             frame_context,
         ) {
             for child_pic_index in &children {
                 self.update_picture(
                     *child_pic_index,
                     state,
                     frame_context,
+                    resource_cache,
+                    prim_data_store,
+                    clip_store,
                 );
             }
 
+            self.pictures[pic_index.0].update_prim_dependencies(
+                state,
+                frame_context,
+                resource_cache,
+                prim_data_store,
+                &self.primitives,
+                &self.pictures,
+                clip_store,
+                &self.opacity_bindings,
+            );
+
             self.pictures[pic_index.0].post_update(
                 children,
                 state,
                 frame_context,
+                resource_cache,
             );
         }
     }
 
     pub fn add_primitive(
         &mut self,
         local_rect: &LayoutRect,
         local_clip_rect: &LayoutRect,
@@ -2289,16 +2310,22 @@ impl PrimitiveStore {
                 (prim_data.prim_rect, prim_data.clip_rect)
             }
             PrimitiveInstanceKind::LegacyPrimitive { prim_index } => {
                 let prim = &self.primitives[prim_index.0];
                 (prim.local_rect, prim.local_clip_rect)
             }
         };
 
+        // TODO(gw): Eventually we can move all the code handling below for
+        //           visibility and clip chain building to be done during the
+        //           update_prim_dependencies pass. That will mean that:
+        //           (a) We only do the work if the relative transforms change.
+        //           (b) Local clip rects can reduce the # of tile dependencies.
+
         // TODO(gw): Having this declared outside is a hack / workaround. We
         //           need it in pic.prepare_for_render below, but that code
         //           path will only read it in the !is_passthrough case
         //           below. This should be much tidier once we port this
         //           traversal to work with a state stack like the initial
         //           picture traversal now works.
         let mut clipped_world_rect = WorldRect::zero();
 
@@ -2313,18 +2340,21 @@ impl PrimitiveStore {
                 }
                 return false;
             }
 
             // Inflate the local rect for this primitive by the inflation factor of
             // the picture context. This ensures that even if the primitive itself
             // is not visible, any effects from the blur radius will be correctly
             // taken into account.
+            let inflation_factor = frame_state
+                .surfaces[pic_context.surface_index.0]
+                .inflation_factor;
             let local_rect = prim_local_rect
-                .inflate(pic_context.inflation_factor, pic_context.inflation_factor)
+                .inflate(inflation_factor, inflation_factor)
                 .intersection(&prim_local_clip_rect);
             let local_rect = match local_rect {
                 Some(local_rect) => local_rect,
                 None => {
                     if prim_instance.is_chased() {
                         println!("\tculled for being out of the local clip rectangle: {:?}",
                             prim_local_clip_rect);
                     }
@@ -2340,17 +2370,17 @@ impl PrimitiveStore {
                     prim_local_clip_rect,
                     prim_context.spatial_node_index,
                     &pic_state.map_local_to_pic,
                     &pic_state.map_pic_to_world,
                     &frame_context.clip_scroll_tree,
                     frame_state.gpu_cache,
                     frame_state.resource_cache,
                     frame_context.device_pixel_scale,
-                    &frame_context.world_rect,
+                    &pic_context.dirty_world_rect,
                     &clip_node_collector,
                     &mut frame_state.resources.clip_data_store,
                 );
 
             let clip_chain = match clip_chain {
                 Some(clip_chain) => clip_chain,
                 None => {
                     if prim_instance.is_chased() {
@@ -2381,33 +2411,33 @@ impl PrimitiveStore {
                 .map(&clip_chain.pic_clip_rect)
             {
                 Some(world_rect) => world_rect,
                 None => {
                     return false;
                 }
             };
 
-            clipped_world_rect = match world_rect.intersection(&frame_context.world_rect) {
+            clipped_world_rect = match world_rect.intersection(&pic_context.dirty_world_rect) {
                 Some(rect) => rect,
                 None => {
                     return false;
                 }
             };
 
             prim_instance.bounding_rect = Some(clip_chain.pic_clip_rect);
 
             prim_instance.update_clip_task(
                 prim_local_rect,
                 prim_local_clip_rect,
                 prim_context,
                 clipped_world_rect,
                 pic_context.raster_spatial_node_index,
                 &clip_chain,
-                pic_context.surface_index,
+                pic_context,
                 pic_state,
                 frame_context,
                 frame_state,
                 &clip_node_collector,
                 &mut self.primitives,
             );
 
             if prim_instance.is_chased() {
@@ -2433,30 +2463,30 @@ impl PrimitiveStore {
                     frame_state,
                 ) {
                     if let Some(ref mut splitter) = pic_state.plane_splitter {
                         PicturePrimitive::add_split_plane(
                             splitter,
                             frame_state.transforms,
                             prim_instance,
                             prim_local_rect,
-                            frame_context.world_rect,
+                            pic_context.dirty_world_rect,
                             plane_split_anchor,
                         );
                     }
                 } else {
                     prim_instance.bounding_rect = None;
                 }
 
                 if let Some(mut request) = frame_state.gpu_cache.request(&mut pic.gpu_location) {
                     request.push(PremultipliedColorF::WHITE);
                     request.push(PremultipliedColorF::WHITE);
                     request.push([
-                        pic.local_rect.size.width,
-                        pic.local_rect.size.height,
+                        -1.0,       // -ve means use prim rect for stretch size
+                        0.0,
                         0.0,
                         0.0,
                     ]);
                     request.write_segment(
                         pic.local_rect,
                         [0.0; 4],
                     );
                 }
@@ -2474,17 +2504,17 @@ impl PrimitiveStore {
             }
             PrimitiveInstanceKind::LegacyPrimitive { prim_index } => {
                 let prim_details = &mut self.primitives[prim_index.0].details;
 
                 prim_instance.prepare_prim_for_render_inner(
                     prim_local_rect,
                     prim_details,
                     prim_context,
-                    pic_context.surface_index,
+                    pic_context,
                     pic_state,
                     frame_context,
                     frame_state,
                     display_list,
                     &mut self.opacity_bindings,
                 );
             }
         }
@@ -2982,17 +3012,17 @@ impl PrimitiveInstance {
     fn update_clip_task_for_brush(
         &mut self,
         prim_local_rect: LayoutRect,
         prim_local_clip_rect: LayoutRect,
         root_spatial_node_index: SpatialNodeIndex,
         prim_bounding_rect: WorldRect,
         prim_context: &PrimitiveContext,
         prim_clip_chain: &ClipChainInstance,
-        surface_index: SurfaceIndex,
+        pic_context: &PictureContext,
         pic_state: &mut PictureState,
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
         clip_node_collector: &Option<ClipNodeCollector>,
         primitives: &mut [Primitive],
     ) -> bool {
         let brush = match self.kind {
             PrimitiveInstanceKind::Picture { .. } |
@@ -3036,17 +3066,17 @@ impl PrimitiveInstance {
         // the clip chain builder. Instead, just use the clip chain
         // instance that was built for the main primitive. This is a
         // significant optimization for the common case.
         if segment_desc.segments.len() == 1 {
             let clip_mask_kind = segment_desc.segments[0].update_clip_task(
                 Some(prim_clip_chain),
                 prim_bounding_rect,
                 root_spatial_node_index,
-                surface_index,
+                pic_context.surface_index,
                 pic_state,
                 frame_context,
                 frame_state,
             );
             frame_state.scratch.clip_mask_instances.push(clip_mask_kind);
         } else {
             for segment in &mut segment_desc.segments {
                 // Build a clip chain for the smaller segment rect. This will
@@ -3060,43 +3090,43 @@ impl PrimitiveInstance {
                         prim_local_clip_rect,
                         prim_context.spatial_node_index,
                         &pic_state.map_local_to_pic,
                         &pic_state.map_pic_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_context.device_pixel_scale,
-                        &frame_context.world_rect,
+                        &pic_context.dirty_world_rect,
                         clip_node_collector,
                         &mut frame_state.resources.clip_data_store,
                     );
 
                 let clip_mask_kind = segment.update_clip_task(
                     segment_clip_chain.as_ref(),
                     prim_bounding_rect,
                     root_spatial_node_index,
-                    surface_index,
+                    pic_context.surface_index,
                     pic_state,
                     frame_context,
                     frame_state,
                 );
                 frame_state.scratch.clip_mask_instances.push(clip_mask_kind);
             }
         }
 
         true
     }
 
     fn prepare_prim_for_render_inner(
         &mut self,
         prim_local_rect: LayoutRect,
         prim_details: &mut PrimitiveDetails,
         prim_context: &PrimitiveContext,
-        surface_index: SurfaceIndex,
+        pic_context: &PictureContext,
         pic_state: &mut PictureState,
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
         display_list: &BuiltDisplayList,
         opacity_bindings: &mut OpacityBindingStorage,
     ) {
         let mut is_tiled = false;
 
@@ -3213,17 +3243,17 @@ impl PrimitiveInstance {
                                                 *size,
                                                 BlitSource::RenderTask {
                                                     task_id: cache_to_target_task_id,
                                                 },
                                             );
                                             let target_to_cache_task_id = render_tasks.add(target_to_cache_task);
 
                                             // Hook this into the render task tree at the right spot.
-                                            surfaces[surface_index.0].tasks.push(target_to_cache_task_id);
+                                            surfaces[pic_context.surface_index.0].tasks.push(target_to_cache_task_id);
 
                                             // Pass the image opacity, so that the cached render task
                                             // item inherits the same opacity properties.
                                             target_to_cache_task_id
                                         }
                                     ));
                                 }
                                 ImageSource::Default => {
@@ -3239,17 +3269,17 @@ impl PrimitiveInstance {
                                 // produce primitives that are partially covering the original image
                                 // rect and we want to clip these extra parts out.
                                 let tight_clip_rect = self
                                     .combined_local_clip_rect
                                     .intersection(&prim_local_rect).unwrap();
 
                                 let visible_rect = compute_conservative_visible_rect(
                                     prim_context,
-                                    &frame_context.world_rect,
+                                    &pic_context.dirty_world_rect,
                                     &tight_clip_rect
                                 );
 
                                 let base_edge_flags = edge_flags_for_tile_spacing(tile_spacing);
 
                                 let stride = stretch_size + *tile_spacing;
 
                                 visible_tiles.clear();
@@ -3389,17 +3419,17 @@ impl PrimitiveInstance {
                                                     &segment.cache_key,
                                                     border,
                                                     scale,
                                                 ),
                                             );
 
                                             let task_id = render_tasks.add(task);
 
-                                            surfaces[surface_index.0].tasks.push(task_id);
+                                            surfaces[pic_context.surface_index.0].tasks.push(task_id);
 
                                             task_id
                                         }
                                     ));
                                 }
 
                                 // Shouldn't matter, since the segment opacity is used instead
                                 PrimitiveOpacity::translucent()
@@ -3433,17 +3463,17 @@ impl PrimitiveInstance {
                             decompose_repeated_primitive(
                                 visible_tiles,
                                 self,
                                 &prim_local_rect,
                                 &stretch_size,
                                 &tile_spacing,
                                 prim_context,
                                 frame_state,
-                                &frame_context.world_rect,
+                                &pic_context.dirty_world_rect,
                                 &mut |rect, mut request| {
                                     request.push([
                                         center.x,
                                         center.y,
                                         start_radius,
                                         end_radius,
                                     ]);
                                     request.push([
@@ -3487,17 +3517,17 @@ impl PrimitiveInstance {
                             decompose_repeated_primitive(
                                 visible_tiles,
                                 self,
                                 &prim_local_rect,
                                 &stretch_size,
                                 &tile_spacing,
                                 prim_context,
                                 frame_state,
-                                &frame_context.world_rect,
+                                &pic_context.dirty_world_rect,
                                 &mut |rect, mut request| {
                                     request.push([
                                         start_point.x,
                                         start_point.y,
                                         end_point.x,
                                         end_point.y,
                                     ]);
                                     request.push([
@@ -3555,17 +3585,17 @@ impl PrimitiveInstance {
     fn update_clip_task(
         &mut self,
         prim_local_rect: LayoutRect,
         prim_local_clip_rect: LayoutRect,
         prim_context: &PrimitiveContext,
         prim_bounding_rect: WorldRect,
         root_spatial_node_index: SpatialNodeIndex,
         clip_chain: &ClipChainInstance,
-        surface_index: SurfaceIndex,
+        pic_context: &PictureContext,
         pic_state: &mut PictureState,
         frame_context: &FrameBuildingContext,
         frame_state: &mut FrameBuildingState,
         clip_node_collector: &Option<ClipNodeCollector>,
         primitives: &mut [Primitive],
     ) {
         if self.is_chased() {
             println!("\tupdating clip task with pic rect {:?}", clip_chain.pic_clip_rect);
@@ -3577,17 +3607,17 @@ impl PrimitiveInstance {
         // First try to  render this primitive's mask using optimized brush rendering.
         if self.update_clip_task_for_brush(
             prim_local_rect,
             prim_local_clip_rect,
             root_spatial_node_index,
             prim_bounding_rect,
             prim_context,
             &clip_chain,
-            surface_index,
+            pic_context,
             pic_state,
             frame_context,
             frame_state,
             clip_node_collector,
             primitives,
         ) {
             if self.is_chased() {
                 println!("\tsegment tasks have been created for clipping");
@@ -3618,17 +3648,17 @@ impl PrimitiveInstance {
                 if self.is_chased() {
                     println!("\tcreated task {:?} with device rect {:?}",
                         clip_task_id, device_rect);
                 }
                 // Set the global clip mask instance for this primitive.
                 let clip_task_index = ClipTaskIndex(frame_state.scratch.clip_mask_instances.len() as _);
                 frame_state.scratch.clip_mask_instances.push(ClipMaskKind::Mask(clip_task_id));
                 self.clip_task_index = clip_task_index;
-                frame_state.surfaces[surface_index.0].tasks.push(clip_task_id);
+                frame_state.surfaces[pic_context.surface_index.0].tasks.push(clip_task_id);
             }
         }
     }
 }
 
 pub fn get_raster_rects(
     pic_rect: PictureRect,
     map_to_raster: &SpaceMapper<PicturePixel, RasterPixel>,
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DeviceSize, DeviceIntSideOffsets};
 use api::{DevicePixelScale, ImageDescriptor, ImageFormat};
-use api::{LineStyle, LineOrientation, LayoutSize};
+use api::{LineStyle, LineOrientation, LayoutSize, ColorF};
 #[cfg(feature = "pathfinder")]
 use api::FontRenderMode;
 use border::{BorderCornerCacheKey, BorderEdgeCacheKey};
 use box_shadow::{BoxShadowCacheKey};
 use clip::{ClipDataStore, ClipItem, ClipStore, ClipNodeRange};
 use clip_scroll_tree::SpatialNodeIndex;
 use device::TextureFilter;
 #[cfg(feature = "pathfinder")]
@@ -249,23 +249,32 @@ pub struct CacheMaskTask {
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipRegionTask {
     pub clip_data_address: GpuCacheAddress,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileBlit {
+    pub target: CacheItem,
+    pub offset: DeviceIntPoint,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PictureTask {
     pub pic_index: PictureIndex,
     pub can_merge: bool,
     pub content_origin: DeviceIntPoint,
     pub uv_rect_handle: GpuCacheHandle,
     pub root_spatial_node_index: SpatialNodeIndex,
     uv_rect_kind: UvRectKind,
+    pub blits: Vec<TileBlit>,
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BlurTask {
     pub blur_std_deviation: f32,
     pub target_kind: RenderTargetKind,
@@ -367,26 +376,27 @@ pub enum RenderTaskKind {
     Glyph(GlyphTask),
     Readback(DeviceIntRect),
     Scaling(ScalingTask),
     Blit(BlitTask),
     Border(BorderTask),
     LineDecoration(LineDecorationTask),
 }
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ClearMode {
     // Applicable to color and alpha targets.
     Zero,
     One,
 
     // Applicable to color targets only.
     Transparent,
+    Color(ColorF),
 }
 
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTask {
     pub location: RenderTaskLocation,
     pub children: Vec<RenderTaskId>,
@@ -417,40 +427,48 @@ impl RenderTask {
     pub fn new_picture(
         location: RenderTaskLocation,
         unclipped_size: DeviceSize,
         pic_index: PictureIndex,
         content_origin: DeviceIntPoint,
         children: Vec<RenderTaskId>,
         uv_rect_kind: UvRectKind,
         root_spatial_node_index: SpatialNodeIndex,
+        clear_color: Option<ColorF>,
+        blits: Vec<TileBlit>,
     ) -> Self {
         let size = match location {
             RenderTaskLocation::Dynamic(_, size) => size,
             RenderTaskLocation::Fixed(rect) => rect.size,
             RenderTaskLocation::TextureCache { rect, .. } => rect.size,
         };
 
         render_task_sanity_check(&size);
 
         let can_merge = size.width as f32 >= unclipped_size.width &&
                         size.height as f32 >= unclipped_size.height;
 
+        let clear_mode = match clear_color {
+            Some(color) => ClearMode::Color(color),
+            None => ClearMode::Transparent,
+        };
+
         RenderTask {
             location,
             children,
             kind: RenderTaskKind::Picture(PictureTask {
                 pic_index,
                 content_origin,
                 can_merge,
                 uv_rect_handle: GpuCacheHandle::new(),
                 uv_rect_kind,
                 root_spatial_node_index,
+                blits,
             }),
-            clear_mode: ClearMode::Transparent,
+            clear_mode,
             saved_index: None,
         }
     }
 
     pub fn new_readback(screen_rect: DeviceIntRect) -> Self {
         RenderTask::with_dynamic_location(
             screen_rect.size,
             Vec::new(),
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -59,16 +59,17 @@ use internal_types::{TextureCacheAllocat
 use internal_types::{RenderTargetInfo, SavedTargetIndex};
 use prim_store::DeferredResolve;
 use profiler::{BackendProfileCounters, FrameProfileCounters, TimeProfileCounter,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
 use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
 use render_backend::{FrameId, RenderBackend};
+use render_task::ClearMode;
 use scene_builder::{SceneBuilder, LowPrioritySceneBuilder};
 use shade::{Shaders, WrShaders};
 use smallvec::SmallVec;
 use render_task::{RenderTask, RenderTaskKind, RenderTaskTree};
 use resource_cache::ResourceCache;
 use util::drain_filter;
 
 use std;
@@ -3202,16 +3203,32 @@ impl Renderer {
                 // Note: at this point, the target rectangle is not guaranteed to be within the main framebuffer bounds
                 // but `clear_target_rect` is totally fine with negative origin, as long as width & height are positive
                 rect.origin.y = draw_target.dimensions().height as i32 - rect.origin.y - rect.size.height;
                 Some(rect)
             };
 
             self.device.clear_target(clear_color, depth_clear, clear_rect);
 
+            // If this color target requires any tasks to be pre-cleared,
+            // go through and do that now.
+            for &task_id in &target.color_clears {
+                let task = &render_tasks[task_id];
+                let (rect, _) = task.get_target_rect();
+                let color = match task.clear_mode {
+                    ClearMode::Color(color) => color.to_array(),
+                    _ => unreachable!(),
+                };
+                self.device.clear_target(
+                    Some(color),
+                    None,
+                    Some(rect),
+                );
+            }
+
             if depth_clear.is_some() {
                 self.device.disable_depth_write();
             }
         }
 
         // Handle any blits from the texture cache to this target.
         self.handle_blits(&target.blits, render_tasks);
 
@@ -3456,16 +3473,48 @@ impl Renderer {
                 dest_rect.size.height *= -1;
 
                 self.device.bind_read_target(draw_target.into());
                 self.device.bind_external_draw_target(fbo_id);
                 self.device.blit_render_target(src_rect, dest_rect);
                 handler.unlock(output.pipeline_id);
             }
         }
+
+        // At the end of rendering a target, blit across any cache tiles
+        // to the texture cache for use on subsequent frames.
+        if !target.tile_blits.is_empty() {
+            let _timer = self.gpu_profile.start_timer(GPU_TAG_BLIT);
+
+            self.device.bind_read_target(draw_target.into());
+
+            for blit in &target.tile_blits {
+                let texture = self.texture_resolver
+                    .resolve(&blit.target.texture_id)
+                    .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 dest_rect = blit.target.uv_rect.to_i32();
+
+                self.device.blit_render_target(
+                    src_rect,
+                    dest_rect,
+                );
+            }
+        }
     }
 
     fn draw_alpha_target(
         &mut self,
         draw_target: DrawTarget,
         target: &AlphaRenderTarget,
         projection: &Transform3D<f32>,
         render_tasks: &RenderTaskTree,
--- a/gfx/wr/webrender/src/resource_cache.rs
+++ b/gfx/wr/webrender/src/resource_cache.rs
@@ -61,16 +61,18 @@ pub struct GlyphFetchResult {
 // values in the vertex shader. The reason
 // for this is that the texture may change
 // dimensions (e.g. the pages in a texture
 // atlas can grow). When this happens, by
 // storing the coordinates as texel values
 // we don't need to go through and update
 // various CPU-side structures.
 #[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CacheItem {
     pub texture_id: TextureSource,
     pub uv_rect_handle: GpuCacheHandle,
     pub uv_rect: DeviceIntRect,
     pub texture_layer: i32,
 }
 
 impl CacheItem {
@@ -403,17 +405,17 @@ pub struct ResourceCache {
     cached_glyphs: GlyphCache,
     cached_images: ImageCache,
     cached_render_tasks: RenderTaskCache,
 
     resources: Resources,
     state: State,
     current_frame_id: FrameId,
 
-    texture_cache: TextureCache,
+    pub texture_cache: TextureCache,
 
     // TODO(gw): We should expire (parts of) this cache semi-regularly!
     cached_glyph_dimensions: GlyphDimensionsCache,
     glyph_rasterizer: GlyphRasterizer,
 
     // The set of images that aren't present or valid in the texture cache,
     // and need to be rasterized and/or uploaded this frame. This includes
     // both blobs and regular images.
@@ -876,16 +878,42 @@ impl ResourceCache {
             },
             None => {
                 warn!("Delete the non-exist key");
                 debug!("key={:?}", image_key);
             }
         }
     }
 
+    /// Check if an image has changed since it was last requested.
+    pub fn is_image_dirty(
+        &self,
+        image_key: ImageKey,
+    ) -> bool {
+        match self.cached_images.try_get(&image_key) {
+            Some(ImageResult::UntiledAuto(ref info)) => {
+                info.dirty_rect.is_some()
+            }
+            Some(ImageResult::Multi(ref entries)) => {
+                for (_, entry) in &entries.resources {
+                    if entry.dirty_rect.is_some() {
+                        return true;
+                    }
+                }
+                false
+            }
+            Some(ImageResult::Err(..)) => {
+                false
+            }
+            None => {
+                true
+            }
+        }
+    }
+
     pub fn request_image(
         &mut self,
         request: ImageRequest,
         gpu_cache: &mut GpuCache,
     ) {
         debug_assert_eq!(self.state, State::AddResources);
 
         let template = match self.resources.image_templates.get(request.key) {
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.rs
@@ -107,16 +107,25 @@ impl SceneProperties {
             PropertyBinding::Binding(ref key, v) => {
                 self.float_properties
                     .get(&key.id)
                     .cloned()
                     .unwrap_or(v)
             }
         }
     }
+
+    pub fn get_float_value(
+        &self,
+        id: PropertyBindingId,
+    ) -> Option<f32> {
+        self.float_properties
+            .get(&id)
+            .cloned()
+    }
 }
 
 /// A representation of the layout within the display port for a given document or iframe.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone)]
 pub struct ScenePipeline {
     pub pipeline_id: PipelineId,
--- a/gfx/wr/webrender/src/surface.rs
+++ b/gfx/wr/webrender/src/surface.rs
@@ -295,17 +295,17 @@ impl SurfaceDescriptor {
                 // of validating the surface cache contents.
                 let raster_spatial_node = &clip_scroll_tree.spatial_nodes[raster_spatial_node_index.0];
                 let surface_spatial_node = &clip_scroll_tree.spatial_nodes[surface_spatial_node_index.0];
 
                 let mut key = CoordinateSpaceMapping::<LayoutPixel, PicturePixel>::new(
                     raster_spatial_node_index,
                     surface_spatial_node_index,
                     clip_scroll_tree,
-                ).into();
+                ).expect("bug: unable to get coord mapping").into();
 
                 if let TransformKey::ScaleOffset(ref mut key) = key {
                     if raster_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id {
                         key.offset_x = 0.0;
                         key.offset_y = 0.0;
                     }
                 }
 
@@ -320,12 +320,12 @@ impl SurfaceDescriptor {
         for (spatial_node_index, transform) in self.spatial_nodes
             .iter()
             .zip(self.cache_key.transforms.iter_mut())
         {
             *transform = CoordinateSpaceMapping::<LayoutPixel, PicturePixel>::new(
                 raster_spatial_node_index,
                 *spatial_node_index,
                 clip_scroll_tree,
-            ).into();
+            ).expect("bug: unable to get coord mapping").into();
         }
     }
 }
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -16,17 +16,17 @@ use gpu_types::{BorderInstance, BlurDire
 use gpu_types::{TransformData, TransformPalette, ZBufferIdGenerator};
 use internal_types::{CacheTextureId, FastHashMap, SavedTargetIndex, TextureSource};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
 use picture::SurfaceInfo;
 use prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
 use profiler::FrameProfileCounters;
 use render_backend::{FrameId, FrameResources};
-use render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind};
+use render_task::{BlitSource, RenderTaskAddress, RenderTaskId, RenderTaskKind, TileBlit};
 use render_task::{BlurTask, ClearMode, GlyphTask, RenderTaskLocation, RenderTaskTree, ScalingTask};
 use resource_cache::ResourceCache;
 use std::{cmp, usize, f32, i32, mem};
 use texture_allocator::GuillotineAllocator;
 #[cfg(feature = "pathfinder")]
 use webrender_api::{DevicePixel, FontRenderMode};
 
 const STYLE_SOLID: i32 = ((BorderStyle::Solid as i32) << 8) | ((BorderStyle::Solid as i32) << 16);
@@ -382,16 +382,18 @@ pub struct ColorRenderTarget {
     // List of blur operations to apply for this render target.
     pub vertical_blurs: Vec<BlurInstance>,
     pub horizontal_blurs: Vec<BlurInstance>,
     pub readbacks: Vec<DeviceIntRect>,
     pub scalings: Vec<ScalingInstance>,
     pub blits: Vec<BlitJob>,
     // List of frame buffer outputs for this render target.
     pub outputs: Vec<FrameOutput>,
+    pub tile_blits: Vec<TileBlit>,
+    pub color_clears: Vec<RenderTaskId>,
     allocator: Option<TextureAllocator>,
     alpha_tasks: Vec<RenderTaskId>,
     screen_size: DeviceIntSize,
 }
 
 impl RenderTarget for ColorRenderTarget {
     fn allocate(&mut self, size: DeviceIntSize) -> Option<DeviceIntPoint> {
         self.allocator
@@ -409,16 +411,18 @@ impl RenderTarget for ColorRenderTarget 
             vertical_blurs: Vec::new(),
             horizontal_blurs: Vec::new(),
             readbacks: Vec::new(),
             scalings: Vec::new(),
             blits: Vec::new(),
             allocator: size.map(TextureAllocator::new),
             outputs: Vec::new(),
             alpha_tasks: Vec::new(),
+            color_clears: Vec::new(),
+            tile_blits: Vec::new(),
             screen_size,
         }
     }
 
     fn build(
         &mut self,
         ctx: &mut RenderTargetContext,
         gpu_cache: &mut GpuCache,
@@ -428,16 +432,27 @@ impl RenderTarget for ColorRenderTarget 
         transforms: &mut TransformPalette,
         z_generator: &mut ZBufferIdGenerator,
     ) {
         let mut merged_batches = AlphaBatchContainer::new(None);
 
         for task_id in &self.alpha_tasks {
             let task = &render_tasks[*task_id];
 
+            match task.clear_mode {
+                ClearMode::One |
+                ClearMode::Zero => {
+                    panic!("bug: invalid clear mode for color task");
+                }
+                ClearMode::Transparent => {}
+                ClearMode::Color(..) => {
+                    self.color_clears.push(*task_id);
+                }
+            }
+
             match task.kind {
                 RenderTaskKind::Picture(ref pic_task) => {
                     let pic = &ctx.prim_store.pictures[pic_task.pic_index.0];
 
                     let (target_rect, _) = task.get_target_rect();
 
                     let mut batch_builder = AlphaBatchBuilder::new(
                         self.screen_size,
@@ -453,16 +468,26 @@ impl RenderTarget for ColorRenderTarget 
                         render_tasks,
                         deferred_resolves,
                         prim_headers,
                         transforms,
                         pic_task.root_spatial_node_index,
                         z_generator,
                     );
 
+                    for blit in &pic_task.blits {
+                        self.tile_blits.push(TileBlit {
+                            target: blit.target.clone(),
+                            offset: DeviceIntPoint::new(
+                                blit.offset.x + target_rect.origin.x,
+                                blit.offset.y + target_rect.origin.y,
+                            ),
+                        })
+                    }
+
                     if let Some(batch_container) = batch_builder.build(&mut merged_batches) {
                         self.alpha_batch_containers.push(batch_container);
                     }
                 }
                 _ => {
                     unreachable!();
                 }
             }
@@ -636,16 +661,17 @@ impl RenderTarget for AlphaRenderTarget 
     ) {
         let task = &render_tasks[task_id];
 
         match task.clear_mode {
             ClearMode::Zero => {
                 self.zero_clears.push(task_id);
             }
             ClearMode::One => {}
+            ClearMode::Color(..) |
             ClearMode::Transparent => {
                 panic!("bug: invalid clear mode for alpha task");
             }
         }
 
         match task.kind {
             RenderTaskKind::Readback(..) |
             RenderTaskKind::Picture(..) |
--- a/gfx/wr/wrench/src/reftest.rs
+++ b/gfx/wr/wrench/src/reftest.rs
@@ -390,37 +390,40 @@ impl<'a> ReftestHarness<'a> {
                     DebugCommand::EnableDualSourceBlending(true)
                 );
         }
 
         let comparison = test.compare(&reference);
 
         if let Some(expected_draw_calls) = t.expected_draw_calls {
             if expected_draw_calls != stats.total_draw_calls {
-                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | expected_draw_calls",
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | {}/{} | expected_draw_calls",
+                    t,
                     stats.total_draw_calls,
                     expected_draw_calls
                 );
                 println!("REFTEST TEST-END | {}", t);
                 return false;
             }
         }
         if let Some(expected_alpha_targets) = t.expected_alpha_targets {
             if expected_alpha_targets != stats.alpha_target_count {
-                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | alpha_target_count",
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | {}/{} | alpha_target_count",
+                    t,
                     stats.alpha_target_count,
                     expected_alpha_targets
                 );
                 println!("REFTEST TEST-END | {}", t);
                 return false;
             }
         }
         if let Some(expected_color_targets) = t.expected_color_targets {
             if expected_color_targets != stats.color_target_count {
-                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | color_target_count",
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | {}/{} | color_target_count",
+                    t,
                     stats.color_target_count,
                     expected_color_targets
                 );
                 println!("REFTEST TEST-END | {}", t);
                 return false;
             }
         }