Bug 1476636. Update webrender to commit 9f21ee5dba0694818a1e2e46d95734ede281447c
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Fri, 20 Jul 2018 17:24:47 -0400
changeset 427564 5fdc7f00aba11df0fd5ebefa73050d7659a31abc
parent 427563 8984f6868e572079cc991492124ba99747f0f681
child 427565 7041b2f0aef8df57cd4b33ee054dd866422ff61d
push id105499
push userjmuizelaar@mozilla.com
push dateFri, 20 Jul 2018 21:25:45 +0000
treeherdermozilla-inbound@80efe717b563 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1476636
milestone63.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 1476636. Update webrender to commit 9f21ee5dba0694818a1e2e46d95734ede281447c
gfx/webrender/Cargo.toml
gfx/webrender/doc/blob.md
gfx/webrender/src/batch.rs
gfx/webrender/src/clip_node.rs
gfx/webrender/src/clip_scroll_tree.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/frame_builder.rs
gfx/webrender/src/glyph_rasterizer/mod.rs
gfx/webrender/src/glyph_rasterizer/pathfinder.rs
gfx/webrender/src/hit_test.rs
gfx/webrender/src/image.rs
gfx/webrender/src/picture.rs
gfx/webrender/src/platform/macos/font.rs
gfx/webrender/src/platform/unix/font.rs
gfx/webrender/src/platform/windows/font.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/renderer.rs
gfx/webrender/src/resource_cache.rs
gfx/webrender/src/scene_builder.rs
gfx/webrender/src/spatial_node.rs
gfx/webrender/src/tiling.rs
gfx/webrender/src/util.rs
gfx/webrender_api/Cargo.toml
gfx/webrender_api/src/api.rs
gfx/webrender_api/src/image.rs
gfx/webrender_api/src/units.rs
gfx/webrender_bindings/Cargo.toml
gfx/webrender_bindings/revision.txt
gfx/wrench/Cargo.toml
gfx/wrench/src/angle.rs
gfx/wrench/src/blob.rs
gfx/wrench/src/egl.rs
gfx/wrench/src/json_frame_writer.rs
gfx/wrench/src/main.rs
gfx/wrench/src/rawtest.rs
gfx/wrench/src/ron_frame_writer.rs
gfx/wrench/src/wrench.rs
gfx/wrench/src/yaml_frame_writer.rs
--- a/gfx/webrender/Cargo.toml
+++ b/gfx/webrender/Cargo.toml
@@ -19,24 +19,24 @@ serialize_program = ["serde"]
 
 [dependencies]
 app_units = "0.6"
 base64 = { optional = true, version = "0.6" }
 bincode = "1.0"
 bitflags = "1.0"
 byteorder = "1.0"
 cfg-if = "0.1.2"
-euclid = "0.17.3"
+euclid = "0.18"
 fxhash = "0.2.1"
-gleam = "0.5"
+gleam = "0.6"
 image = { optional = true, version = "0.19" }
 lazy_static = "1"
 log = "0.4"
 num-traits = "0.1.43"
-plane-split = "0.9.1"
+plane-split = "0.10"
 png = { optional = true, version = "0.12" }
 rayon = "1"
 ron = { optional = true, version = "0.1.7" }
 serde = { optional = true, version = "1.0", features = ["serde_derive"] }
 serde_json = { optional = true, version = "1.0" }
 smallvec = "0.6"
 thread_profiler = "0.1.1"
 time = "0.1"
new file mode 100644
--- /dev/null
+++ b/gfx/webrender/doc/blob.md
@@ -0,0 +1,17 @@
+# Blob images
+
+The blob image mechanism now has two traits:
+- [`BlobImageHandler`](https://github.com/servo/webrender/pull/2785/files#diff-2b72a28a40b83edf41a59adfd46b1a11R188) is roughly the equivalent of the previous `BlobImageRenderer` except that it doesn't do any rendering (it manages the state of the blob commands, and resources like fonts).
+- [`AsyncBlobImageRasterizer`](https://github.com/servo/webrender/pull/2785/files#diff-2b72a28a40b83edf41a59adfd46b1a11R211) is created by the handler and sent over to the scene builder thread. the async rasterizer is meant to be a snapshot of the state of blob image commands that can execute the commands if provided some requests.
+
+When receiving a transaction, the render backend / resource cache look at the list of added and updated blob images in that transaction, [collect the list of blob images and tiles that need to be rendered](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R848), create a rasterizer, and ship the two to the scene builder.
+After building the scene the rasterizer gets handed the list of blob requests and [does all of the rasterization](https://github.com/servo/webrender/pull/2785/files#diff-856af4d4ff2333d4204e7e5a87a93c58R153), blocking the scene builder thread until the work is done.
+
+When the scene building and rasterization is done, the render backend receives the rasterized blobs and [stores them](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R520) so that they are available when frame building needs them.
+
+Because blob images can be huge, we don't always want to rasterize them entirely during scene building. To decide what should be rasterized, we rely on gecko giving us a hint through the added `set_image_visible_area` API. When the render backend receives that message [it decides which tiles are going to be rasterized](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R469). This information is also used to [decide which tiles to evict](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R430), so that we don't keep thousands of tiles if we scroll through a massive blob image. The idea is for the visible area to correspond to the size of the display list.
+
+Sometimes, however, Gecko gets this visible area "wrong", or at least gives webrender a certain visible area but eventually webrender requests tiles during frame building that weren't in that area. I think that this is inevitable because the culling logic in gecko and webrender works very differently, so relying on them to match exactly is fragile at best.
+So to work around this type of situation, [keep around the async blob rasterizer](https://github.com/servo/webrender/pull/2785/files#diff-3722af8f0bcba9c3ce197a9aa3052014R769) that we sent to the scene builder, and store it in the resource cache when we swap the scene. This blob rasterizer represents the state of the blob commands at the time the transaction was built (and is potentially different from the state of the blob image handler). Frame building [collects a list of blob images](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R811) (or blob tiles) that are not already rasterized, and asks the current async blob rasterizer to rasterize them synchronously on the render backend. The hope is that this would happen rarely.
+
+Another important detail is that for this to work, resources that are used by blob images (so currently only fonts), need to be in sync with the blobs. Fortunately, fonts are currently immutable so we mostly need to make sure they are added [before the transaction](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R440) is built and [removed after](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R400) the transaction is swapped. If blob images were to use images, then we'd have to either do the same for these images (and disallow updating them), or maintain the state of images before and after scene building like we effectively do for blobs.
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -665,27 +665,30 @@ impl AlphaBatchBuilder {
 
                         // If this picture is participating in a 3D rendering context,
                         // then don't add it to any batches here. Instead, create a polygon
                         // for it and add it to the current plane splitter.
                         if picture.is_in_3d_context {
                             // Push into parent plane splitter.
                             debug_assert!(picture.surface.is_some());
 
-                            let real_xf = &ctx.clip_scroll_tree
-                                .spatial_nodes[picture.reference_frame_index.0]
-                                .world_content_transform
-                                .into();
-                            let polygon = make_polygon(
+                            let real_xf = &ctx
+                                .transforms
+                                .get_transform(picture.reference_frame_index);
+                            match make_polygon(
                                 picture.real_local_rect,
-                                real_xf,
+                                &real_xf.m,
                                 prim_index.0,
-                            );
-
-                            splitter.add(polygon);
+                            ) {
+                                Some(polygon) => splitter.add(polygon),
+                                None => {
+                                    // this shouldn't happen, the path will ultimately be
+                                    // turned into `expect` when the splitting code is fixed
+                                }
+                            }
 
                             return;
                         }
 
                         let add_to_parent_pic = match picture.composite_mode {
                             Some(PictureCompositeMode::Filter(filter)) => {
                                 assert!(filter.is_visible());
                                 match filter {
@@ -1692,17 +1695,17 @@ pub fn resolve_image(
 
 /// Construct a polygon from stacking context boundaries.
 /// `anchor` here is an index that's going to be preserved in all the
 /// splits of the polygon.
 fn make_polygon(
     rect: LayoutRect,
     transform: &LayoutToWorldTransform,
     anchor: usize,
-) -> Polygon<f64, WorldPixel> {
+) -> Option<Polygon<f64, WorldPixel>> {
     let mat = TypedTransform3D::row_major(
         transform.m11 as f64,
         transform.m12 as f64,
         transform.m13 as f64,
         transform.m14 as f64,
         transform.m21 as f64,
         transform.m22 as f64,
         transform.m23 as f64,
@@ -1710,17 +1713,17 @@ fn make_polygon(
         transform.m31 as f64,
         transform.m32 as f64,
         transform.m33 as f64,
         transform.m34 as f64,
         transform.m41 as f64,
         transform.m42 as f64,
         transform.m43 as f64,
         transform.m44 as f64);
-    Polygon::from_transformed_rect(rect.cast().unwrap(), mat, anchor)
+    Polygon::from_transformed_rect(rect.cast(), mat, anchor)
 }
 
 /// Batcher managing draw calls into the clip mask (in the RT cache).
 #[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipBatcher {
     /// Rectangle draws fill up the rectangles with rounded corners.
--- a/gfx/webrender/src/clip_node.rs
+++ b/gfx/webrender/src/clip_node.rs
@@ -29,18 +29,18 @@ pub struct ClipNode {
     /// defined ClipChains will still need to access it.
     pub clip_chain_node: Option<ClipChainNode>,
 }
 
 impl ClipNode {
     const EMPTY: ClipNode = ClipNode {
         spatial_node: SpatialNodeIndex(0),
         handle: None,
-        clip_chain_index: ClipChainIndex(0),
-        parent_clip_chain_index: ClipChainIndex(0),
+        clip_chain_index: ClipChainIndex::NO_CLIP,
+        parent_clip_chain_index: ClipChainIndex::NO_CLIP,
         clip_chain_node: None,
     };
 
     pub fn empty() -> ClipNode {
         ClipNode::EMPTY
     }
 
     pub fn update(
@@ -78,17 +78,18 @@ impl ClipNode {
         let new_node = ClipChainNode {
             work_item: ClipWorkItem {
                 spatial_node_index: self.spatial_node,
                 clip_sources: weak_handle,
                 coordinate_system_id: spatial_node.coordinate_system_id,
             },
             local_clip_rect: spatial_node
                 .coordinate_system_relative_transform
-                .transform_rect(&local_outer_rect),
+                .transform_rect(&local_outer_rect)
+                .expect("clip node transform is not valid"),
             screen_outer_rect,
             screen_inner_rect,
             prev: None,
         };
 
         let mut clip_chain =
             clip_chains[self.parent_clip_chain_index.0]
             .new_with_added_node(&new_node);
--- a/gfx/webrender/src/clip_scroll_tree.rs
+++ b/gfx/webrender/src/clip_scroll_tree.rs
@@ -57,16 +57,20 @@ pub struct ClipChainDescriptor {
     pub index: ClipChainIndex,
     pub parent: Option<ClipChainIndex>,
     pub clips: Vec<ClipNodeIndex>,
 }
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub struct ClipChainIndex(pub usize);
 
+impl ClipChainIndex {
+    pub const NO_CLIP: Self = ClipChainIndex(0);
+}
+
 pub struct ClipScrollTree {
     /// Nodes which determine the positions (offsets and transforms) for primitives
     /// and clips.
     pub spatial_nodes: Vec<SpatialNode>,
 
     /// Nodes which clip primitives.
     pub clip_nodes: Vec<ClipNode>,
 
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -185,16 +185,19 @@ pub struct DisplayListFlattener<'a> {
     sc_stack: Vec<FlattenedStackingContext>,
 
     /// A stack of the current pictures.
     picture_stack: Vec<PictureIndex>,
 
     /// A stack of the currently active shadows
     shadow_stack: Vec<(Shadow, PictureIndex)>,
 
+    /// The stack keeping track of the root clip chains associated with pipelines.
+    pipeline_clip_chain_stack: Vec<ClipChainIndex>,
+
     /// A list of scrollbar primitives.
     pub scrollbar_prims: Vec<ScrollbarPrimitive>,
 
     /// The store of primitives.
     pub prim_store: PrimitiveStore,
 
     /// Information about all primitives involved in hit testing.
     pub hit_testing_runs: Vec<HitTestingRun>,
@@ -235,16 +238,17 @@ impl<'a> DisplayListFlattener<'a> {
             output_pipelines,
             id_to_index_mapper: ClipIdToIndexMapper::default(),
             hit_testing_runs: recycle_vec(old_builder.hit_testing_runs),
             scrollbar_prims: recycle_vec(old_builder.scrollbar_prims),
             reference_frame_stack: Vec::new(),
             picture_stack: Vec::new(),
             shadow_stack: Vec::new(),
             sc_stack: Vec::new(),
+            pipeline_clip_chain_stack: Vec::new(),
             prim_store: old_builder.prim_store.recycle(),
             clip_store: old_builder.clip_store.recycle(),
         };
 
         flattener.id_to_index_mapper.initialize_for_pipeline(root_pipeline);
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
@@ -525,24 +529,26 @@ impl<'a> DisplayListFlattener<'a> {
             None => {
                 debug_assert!(info.ignore_missing_pipeline);
                 return
             },
         };
 
         self.id_to_index_mapper.initialize_for_pipeline(pipeline);
 
-        self.add_clip_node(
+        //TODO: use or assert on `clip_and_scroll_ids.clip_node_id` ?
+        let clip_chain_index = self.add_clip_node(
             info.clip_id,
             clip_and_scroll_ids.scroll_node_id,
             ClipRegion::create_for_clip_node_with_local_clip(
                 &LocalClip::from(*item.clip_rect()),
                 reference_frame_relative_offset
             ),
         );
+        self.pipeline_clip_chain_stack.push(clip_chain_index);
 
         let bounds = item.rect();
         let origin = *reference_frame_relative_offset + bounds.origin.to_vector();
         self.push_reference_frame(
             ClipId::root_reference_frame(iframe_pipeline_id),
             Some(info.clip_id),
             iframe_pipeline_id,
             None,
@@ -559,16 +565,17 @@ impl<'a> DisplayListFlattener<'a> {
             &iframe_rect,
             &pipeline.content_size,
             ScrollSensitivity::ScriptAndInputEvents,
         );
 
         self.flatten_root(pipeline, &iframe_rect.size);
 
         self.pop_reference_frame();
+        self.pipeline_clip_chain_stack.pop();
     }
 
     fn flatten_item<'b>(
         &'b mut self,
         item: DisplayItemRef<'a, 'b>,
         pipeline_id: PipelineId,
         reference_frame_relative_offset: LayoutVector2D,
     ) -> Option<BuiltDisplayListIter<'a>> {
@@ -728,22 +735,25 @@ impl<'a> DisplayListFlattener<'a> {
                     complex_clips,
                     info.image_mask,
                     &reference_frame_relative_offset,
                 );
                 self.add_clip_node(info.id, clip_and_scroll_ids.scroll_node_id, clip_region);
             }
             SpecificDisplayItem::ClipChain(ref info) => {
                 let items = self.get_clip_chain_items(pipeline_id, item.clip_chain_items())
-                                .iter()
-                                .map(|id| self.id_to_index_mapper.get_clip_node_index(*id))
-                                .collect();
-                let parent = info.parent.map(|id|
-                     self.id_to_index_mapper.get_clip_chain_index(&ClipId::ClipChain(id))
-                );
+                    .iter()
+                    .map(|id| self.id_to_index_mapper.get_clip_node_index(*id))
+                    .collect();
+                let parent = match info.parent {
+                    Some(id) => Some(
+                        self.id_to_index_mapper.get_clip_chain_index(&ClipId::ClipChain(id))
+                    ),
+                    None => self.pipeline_clip_chain_stack.last().cloned(),
+                };
                 let clip_chain_index =
                     self.clip_scroll_tree.add_clip_chain_descriptor(parent, items);
                 self.id_to_index_mapper.add_clip_chain(ClipId::ClipChain(info.id), clip_chain_index);
             },
             SpecificDisplayItem::ScrollFrame(ref info) => {
                 self.flatten_scroll_frame(
                     &item,
                     info,
@@ -901,17 +911,17 @@ impl<'a> DisplayListFlattener<'a> {
         is_backface_visible: bool,
         is_pipeline_root: bool,
         spatial_node: ClipId,
         clipping_node: Option<ClipId>,
         glyph_raster_space: GlyphRasterSpace,
     ) {
         let clip_chain_id = match clipping_node {
             Some(ref clipping_node) => self.id_to_index_mapper.get_clip_chain_index(clipping_node),
-            None => ClipChainIndex(0), // This means no clipping.
+            None => ClipChainIndex::NO_CLIP,
         };
         let clip_and_scroll = ScrollNodeAndClipChain::new(
             self.get_spatial_node_index_for_clip_id(spatial_node),
             clip_chain_id
         );
 
         // Construct the necessary set of Picture primitives
         // to draw this stacking context.
@@ -1219,17 +1229,17 @@ impl<'a> DisplayListFlattener<'a> {
             origin_in_parent_reference_frame,
             pipeline_id,
         );
         self.reference_frame_stack.push((reference_frame_id, index));
 
         match parent_id {
             Some(ref parent_id) =>
                 self.id_to_index_mapper.map_to_parent_clip_chain(reference_frame_id, parent_id),
-            _ => self.id_to_index_mapper.add_clip_chain(reference_frame_id, ClipChainIndex(0)),
+            _ => self.id_to_index_mapper.add_clip_chain(reference_frame_id, ClipChainIndex::NO_CLIP),
         }
         index
     }
 
     pub fn current_reference_frame_index(&self) -> SpatialNodeIndex {
         self.reference_frame_stack.last().unwrap().1
     }
 
@@ -1273,31 +1283,32 @@ impl<'a> DisplayListFlattener<'a> {
         );
     }
 
     pub fn add_clip_node(
         &mut self,
         new_node_id: ClipId,
         parent_id: ClipId,
         clip_region: ClipRegion,
-    ) {
+    ) -> ClipChainIndex {
         let clip_sources = ClipSources::from(clip_region);
         let handle = self.clip_store.insert(clip_sources);
 
         let node_index = self.id_to_index_mapper.get_clip_node_index(new_node_id);
         let parent_clip_chain_index =
             self.id_to_index_mapper.get_clip_chain_index_and_cache_result(&parent_id);
         let spatial_node = self.get_spatial_node_index_for_clip_id(parent_id);
         let clip_chain_index = self.clip_scroll_tree.add_clip_node(
             node_index,
             parent_clip_chain_index,
             spatial_node,
             handle,
         );
         self.id_to_index_mapper.add_clip_chain(new_node_id, clip_chain_index);
+        clip_chain_index
     }
 
     pub fn add_scroll_frame(
         &mut self,
         new_node_id: ClipId,
         parent_id: ClipId,
         external_id: Option<ExternalScrollId>,
         pipeline_id: PipelineId,
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -67,16 +67,17 @@ pub struct FrameBuilder {
 
 pub struct FrameBuildingContext<'a> {
     pub scene_id: u64,
     pub device_pixel_scale: DevicePixelScale,
     pub scene_properties: &'a SceneProperties,
     pub pipelines: &'a FastHashMap<PipelineId, Arc<ScenePipeline>>,
     pub screen_rect: DeviceIntRect,
     pub clip_scroll_tree: &'a ClipScrollTree,
+    pub clip_chains: &'a [ClipChain],
     pub transforms: &'a TransformPalette,
     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,
@@ -213,16 +214,17 @@ impl FrameBuilder {
 
         let frame_context = FrameBuildingContext {
             scene_id: self.scene_id,
             device_pixel_scale,
             scene_properties,
             pipelines,
             screen_rect: self.screen_rect.to_i32(),
             clip_scroll_tree,
+            clip_chains: &clip_scroll_tree.clip_chains,
             transforms: transform_palette,
             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),
             ),
         };
 
         let mut frame_state = FrameBuildingState {
@@ -392,17 +394,16 @@ impl FrameBuilder {
         let use_dual_source_blending = self.config.dual_source_blending_is_enabled &&
                                        self.config.dual_source_blending_is_supported;
 
         for pass in &mut passes {
             let mut ctx = RenderTargetContext {
                 device_pixel_scale,
                 prim_store: &self.prim_store,
                 resource_cache,
-                clip_scroll_tree,
                 use_dual_source_blending,
                 transforms: &transform_palette,
             };
 
             pass.build(
                 &mut ctx,
                 gpu_cache,
                 &mut render_tasks,
--- a/gfx/webrender/src/glyph_rasterizer/mod.rs
+++ b/gfx/webrender/src/glyph_rasterizer/mod.rs
@@ -244,17 +244,17 @@ impl FontInstance {
         } else {
             SubpixelDirection::None
         }
     }
 
     #[allow(dead_code)]
     pub fn get_subpx_offset(&self, glyph: &GlyphKey) -> (f64, f64) {
         if self.use_subpixel_position() {
-            let (dx, dy) = glyph.subpixel_offset;
+            let (dx, dy) = glyph.subpixel_offset();
             (dx.into(), dy.into())
         } else {
             (0.0, 0.0)
         }
     }
 
     #[allow(dead_code)]
     pub fn get_glyph_format(&self) -> GlyphFormat {
@@ -282,17 +282,18 @@ impl FontInstance {
         // be scaled up from the size limit to the scaled size.
         // However, this should only occur when the font isn't using any
         // features that would tie it to device space, like transforms,
         // subpixel AA, or subpixel positioning.
         let max_size = self.size.to_f64_px() * x_scale.max(y_scale);
         if max_size > FONT_SIZE_LIMIT &&
            self.transform.is_identity() &&
            self.render_mode != FontRenderMode::Subpixel &&
-           !self.use_subpixel_position() {
+           !self.use_subpixel_position()
+        {
             max_size / FONT_SIZE_LIMIT
         } else {
             1.0
         }
     }
 }
 
 #[repr(u32)]
@@ -367,40 +368,46 @@ impl Into<f64> for SubpixelOffset {
             SubpixelOffset::ThreeQuarters => 0.75,
         }
     }
 }
 
 #[derive(Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-pub struct GlyphKey {
-    pub index: u32,
-    pub subpixel_offset: (SubpixelOffset, SubpixelOffset),
-}
+pub struct GlyphKey(u32);
 
 impl GlyphKey {
     pub fn new(
         index: u32,
         point: DevicePoint,
         subpx_dir: SubpixelDirection,
-    ) -> GlyphKey {
+    ) -> Self {
         let (dx, dy) = match subpx_dir {
             SubpixelDirection::None => (0.0, 0.0),
             SubpixelDirection::Horizontal => (point.x, 0.0),
             SubpixelDirection::Vertical => (0.0, point.y),
             SubpixelDirection::Mixed => (point.x, point.y),
         };
+        let sox = SubpixelOffset::quantize(dx);
+        let soy = SubpixelOffset::quantize(dy);
+        assert_eq!(0, index & 0xF0000000);
 
-        GlyphKey {
-            index,
-            subpixel_offset: (
-                SubpixelOffset::quantize(dx),
-                SubpixelOffset::quantize(dy),
-            ),
+        GlyphKey(index | (sox as u32) << 28 | (soy as u32) << 30)
+    }
+
+    pub fn index(&self) -> GlyphIndex {
+        self.0 & 0x0FFFFFFF
+    }
+
+    fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) {
+        let x = (self.0 >> 28) as u8 & 3;
+        let y = (self.0 >> 30) as u8 & 3;
+        unsafe {
+            (mem::transmute(x), mem::transmute(y))
         }
     }
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[allow(dead_code)]
--- a/gfx/webrender/src/glyph_rasterizer/pathfinder.rs
+++ b/gfx/webrender/src/glyph_rasterizer/pathfinder.rs
@@ -189,19 +189,19 @@ impl GlyphRasterizer {
 
                 let pathfinder_font_instance = pathfinder_font_renderer::FontInstance {
                     font_key: font.font_key.clone(),
                     size: font.size.scale_by(scale.recip()),
                 };
 
                 // TODO: pathfinder will need to support 2D subpixel offset
                 let pathfinder_subpixel_offset =
-                    pathfinder_font_renderer::SubpixelOffset(glyph_key.subpixel_offset.0 as u8);
+                    pathfinder_font_renderer::SubpixelOffset(glyph_key.subpixel_offset().0 as u8);
                 let pathfinder_glyph_key =
-                    pathfinder_font_renderer::GlyphKey::new(glyph_key.index,
+                    pathfinder_font_renderer::GlyphKey::new(glyph_key.index(),
                                                             pathfinder_subpixel_offset);
 
                 if let Ok(glyph_dimensions) =
                         pathfinder_font_context.glyph_dimensions(&pathfinder_font_instance,
                                                                  &pathfinder_glyph_key,
                                                                  false) {
                     let render_task_cache_key = RenderTaskCacheKey {
                         size: TypedSize2D::from_untyped(&glyph_dimensions.size.to_i32()),
@@ -276,19 +276,19 @@ fn request_render_task_from_pathfinder(g
     let size = font.size.scale_by(scale.recip());
     let pathfinder_font_instance = pathfinder_font_renderer::FontInstance {
         font_key: font.font_key.clone(),
         size,
     };
 
     // TODO: pathfinder will need to support 2D subpixel offset
     let pathfinder_subpixel_offset =
-        pathfinder_font_renderer::SubpixelOffset(glyph_key.subpixel_offset.0 as u8);
-    let glyph_subpixel_offset: f64 = glyph_key.subpixel_offset.0.into();
-    let pathfinder_glyph_key = pathfinder_font_renderer::GlyphKey::new(glyph_key.index,
+        pathfinder_font_renderer::SubpixelOffset(glyph_key.subpixel_offset().0 as u8);
+    let glyph_subpixel_offset: f64 = glyph_key.subpixel_offset().0.into();
+    let pathfinder_glyph_key = pathfinder_font_renderer::GlyphKey::new(glyph_key.index(),
                                                                        pathfinder_subpixel_offset);
 
     // TODO(pcwalton): Fall back to CPU rendering if Pathfinder fails to collect the outline.
     let mut mesh = PathfinderMesh::new();
     let outline = try!(font_context.glyph_outline(&pathfinder_font_instance,
                                                   &pathfinder_glyph_key));
     let tolerance = CUBIC_TO_QUADRATIC_APPROX_TOLERANCE;
     mesh.push_stencil_segments(CubicToQuadraticTransformer::new(outline.iter(), tolerance));
--- a/gfx/webrender/src/hit_test.rs
+++ b/gfx/webrender/src/hit_test.rs
@@ -205,18 +205,21 @@ impl HitTester {
         test: &mut HitTest
     ) -> bool {
         if let Some(clipped_in) = test.node_cache.get(&node_index) {
             return *clipped_in == ClippedIn::ClippedIn;
         }
 
         let node = &self.clip_nodes[node_index.0];
         let transform = self.spatial_nodes[node.spatial_node.0].world_viewport_transform;
-        let transformed_point = match transform.inverse() {
-            Some(inverted) => inverted.transform_point2d(&point),
+        let transformed_point = match transform
+            .inverse()
+            .and_then(|inverted| inverted.transform_point2d(&point))
+        {
+            Some(point) => point,
             None => {
                 test.node_cache.insert(node_index, ClippedIn::NotClippedIn);
                 return false;
             }
         };
 
         for region in &node.regions {
             if !region.contains(&transformed_point) {
@@ -231,18 +234,21 @@ impl HitTester {
 
     pub fn find_node_under_point(&self, mut test: HitTest) -> Option<SpatialNodeIndex> {
         let point = test.get_absolute_point(self);
 
         for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
             let spatial_node_index = clip_and_scroll.spatial_node_index;
             let scroll_node = &self.spatial_nodes[spatial_node_index.0];
             let transform = scroll_node.world_content_transform;
-            let point_in_layer = match transform.inverse() {
-                Some(inverted) => inverted.transform_point2d(&point),
+            let point_in_layer = match transform
+                .inverse()
+                .and_then(|inverted| inverted.transform_point2d(&point))
+            {
+                Some(point) => point,
                 None => continue,
             };
 
             let mut clipped_in = false;
             for item in items.iter().rev() {
                 if !item.rect.contains(&point_in_layer) ||
                     !item.clip_rect.contains(&point_in_layer) {
                     continue;
@@ -272,18 +278,21 @@ impl HitTester {
             let pipeline_id = scroll_node.pipeline_id;
             match (test.pipeline_id, pipeline_id) {
                 (Some(id), node_id) if node_id != id => continue,
                 _ => {},
             }
 
             let transform = scroll_node.world_content_transform;
             let mut facing_backwards: Option<bool> = None;  // will be computed on first use
-            let point_in_layer = match transform.inverse() {
-                Some(inverted) => inverted.transform_point2d(&point),
+            let point_in_layer = match transform
+                .inverse()
+                .and_then(|inverted| inverted.transform_point2d(&point))
+            {
+                Some(point) => point,
                 None => continue,
             };
 
             let mut clipped_in = false;
             for item in items.iter().rev() {
                 if !item.rect.contains(&point_in_layer) ||
                     !item.clip_rect.contains(&point_in_layer) {
                     continue;
@@ -303,18 +312,21 @@ impl HitTester {
                     }
                 }
 
                 // We need to calculate the position of the test point relative to the origin of
                 // the pipeline of the hit item. If we cannot get a transformed point, we are
                 // in a situation with an uninvertible transformation so we should just skip this
                 // result.
                 let root_node = &self.spatial_nodes[self.pipeline_root_nodes[&pipeline_id].0];
-                let point_in_viewport = match root_node.world_viewport_transform.inverse() {
-                    Some(inverted) => inverted.transform_point2d(&point),
+                let point_in_viewport = match root_node.world_viewport_transform
+                    .inverse()
+                    .and_then(|inverted| inverted.transform_point2d(&point))
+                {
+                    Some(point) => point,
                     None => continue,
                 };
 
                 result.items.push(HitTestItem {
                     pipeline: pipeline_id,
                     tag: item.tag,
                     point_in_viewport,
                     point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
@@ -406,13 +418,20 @@ impl HitTest {
     }
 
     fn get_absolute_point(&self, hit_tester: &HitTester) -> WorldPoint {
         if !self.flags.contains(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT) {
             return self.point;
         }
 
         let point =  &LayoutPoint::new(self.point.x, self.point.y);
-        self.pipeline_id.map(|id|
-            hit_tester.get_pipeline_root(id).world_viewport_transform.transform_point2d(point)
-        ).unwrap_or_else(|| WorldPoint::new(self.point.x, self.point.y))
+        self.pipeline_id
+            .and_then(|id|
+                hit_tester
+                    .get_pipeline_root(id)
+                    .world_viewport_transform
+                    .transform_point2d(point)
+            )
+            .unwrap_or_else(|| {
+                WorldPoint::new(self.point.x, self.point.y)
+            })
     }
 }
--- a/gfx/webrender/src/image.rs
+++ b/gfx/webrender/src/image.rs
@@ -1,14 +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::{TileOffset, LayoutRect, LayoutSize, LayoutPoint, DeviceUintSize};
-use euclid::vec2;
+use api::{TileOffset, TileRange, LayoutRect, LayoutSize, LayoutPoint};
+use api::{DeviceUintSize, NormalizedRect};
+use euclid::{vec2, point2};
 use prim_store::EdgeAaSegmentMask;
 
 /// If repetitions are far enough apart that only one is within
 /// the primitive rect, then we can simplify the parameters and
 /// treat the primitive as not repeated.
 /// This can let us avoid unnecessary work later to handle some
 /// of the parameters.
 pub fn simplify_repeated_primitive(
@@ -220,16 +221,52 @@ pub fn for_each_tile(
                 edge_flags |= EdgeAaSegmentMask::RIGHT;
             }
 
             callback(&segment_rect, tile_offset, edge_flags);
         }
     }
 }
 
+pub fn compute_tile_range(
+    visible_area: &NormalizedRect,
+    image_size: &DeviceUintSize,
+    tile_size: u16,
+) -> TileRange {
+    // Tile dimensions in normalized coordinates.
+    let tw = (image_size.width as f32) / (tile_size as f32);
+    let th = (image_size.height as f32) / (tile_size as f32);
+
+    let t0 = point2(
+        f32::floor(visible_area.origin.x * tw),
+        f32::floor(visible_area.origin.y * th),
+    ).cast::<u16>();
+
+    let t1 = point2(
+        f32::ceil(visible_area.max_x() * tw),
+        f32::ceil(visible_area.max_y() * th),
+    ).cast::<u16>();
+
+    TileRange {
+        origin: t0,
+        size: (t1 - t0).to_size(),
+    }
+}
+
+pub fn for_each_tile_in_range(
+    range: &TileRange,
+    callback: &mut FnMut(TileOffset),
+) {
+    for y in 0..range.size.height {
+        for x in 0..range.size.width {
+            callback(range.origin + vec2(x, y));
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
     use std::collections::HashSet;
     use api::{LayoutRect, DeviceUintSize};
     use euclid::{rect, size2};
 
     // this checks some additional invariants
--- a/gfx/webrender/src/picture.rs
+++ b/gfx/webrender/src/picture.rs
@@ -74,17 +74,17 @@ pub struct PictureCacheKey {
     //       we want the cache to remain valid as it
     //       is scrolled and/or translated by animation.
     //       This is valid while we have the restriction
     //       in place that only pictures that use the
     //       root coordinate system are cached - once
     //       we relax that, we'll need to consider some
     //       extra parameters, depending on transform.
 
-    // This is a globally unique id of the scene this picture 
+    // This is a globally unique id of the scene this picture
     // is associated with, to avoid picture id collisions.
     scene_id: u64,
 
     // The unique (for the scene_id) identifier for this picture.
     // TODO(gw): Currently, these will not be
     //           shared across new display lists,
     //           so will only remain valid during
     //           scrolling. Next step will be to
@@ -588,17 +588,26 @@ impl PicturePrimitive {
 
 // Calculate a single screen-space UV for a picture.
 fn calculate_screen_uv(
     local_pos: &LayoutPoint,
     transform: &Transform,
     rendered_rect: &DeviceRect,
     device_pixel_scale: DevicePixelScale,
 ) -> DevicePoint {
-    let world_pos = transform.m.transform_point2d(local_pos);
+    let world_pos = match transform.m.transform_point2d(local_pos) {
+        Some(pos) => pos,
+        None => {
+            //Warning: this is incorrect and needs to be fixed properly.
+            // The transformation has put a local vertex behind the near clipping plane...
+            // Proper solution would be to keep the near-clipping-plane results around
+            // (currently produced by calculate_screen_bounding_rect) and use them here.
+            return DevicePoint::new(0.5, 0.5);
+        }
+    };
 
     let mut device_pos = world_pos * device_pixel_scale;
 
     // Apply snapping for axis-aligned scroll nodes, as per prim_shared.glsl.
     if transform.transform_kind == TransformedRectKind::AxisAligned {
         device_pos.x = (device_pos.x + 0.5).floor();
         device_pos.y = (device_pos.y + 0.5).floor();
     }
--- a/gfx/webrender/src/platform/macos/font.rs
+++ b/gfx/webrender/src/platform/macos/font.rs
@@ -356,17 +356,17 @@ impl FontContext {
 
     pub fn get_glyph_dimensions(
         &mut self,
         font: &FontInstance,
         key: &GlyphKey,
     ) -> Option<GlyphDimensions> {
         self.get_ct_font(font.font_key, font.size, &font.variations)
             .and_then(|ref ct_font| {
-                let glyph = key.index as CGGlyph;
+                let glyph = key.index() as CGGlyph;
                 let bitmap = is_bitmap_font(ct_font);
                 let (x_offset, y_offset) = if bitmap { (0.0, 0.0) } else { font.get_subpx_offset(key) };
                 let transform = if font.synthetic_italics.is_enabled() ||
                                    font.flags.intersects(FontInstanceFlags::TRANSPOSE |
                                                          FontInstanceFlags::FLIP_X |
                                                          FontInstanceFlags::FLIP_Y) {
                     let mut shape = FontTransform::identity();
                     if font.flags.contains(FontInstanceFlags::FLIP_X) {
@@ -520,17 +520,17 @@ impl FontContext {
                 d: shape.scale_y as f64,
                 tx: 0.0,
                 ty: 0.0,
             })
         } else {
             None
         };
 
-        let glyph = key.index as CGGlyph;
+        let glyph = key.index() as CGGlyph;
         let (strike_scale, pixel_step) = if bitmap { (y_scale, 1.0) } else { (x_scale, y_scale / x_scale) };
         let extra_strikes = font.get_extra_strikes(strike_scale / scale);
         let metrics = get_glyph_metrics(
             &ct_font,
             transform.as_ref(),
             glyph,
             x_offset,
             y_offset,
--- a/gfx/webrender/src/platform/unix/font.rs
+++ b/gfx/webrender/src/platform/unix/font.rs
@@ -323,17 +323,17 @@ impl FontContext {
                     (req_size * y_scale / scale * 64.0 + 0.5) as FT_F26Dot6,
                     0,
                     0,
                 )
             }
         };
 
         if succeeded(result) {
-            result = unsafe { FT_Load_Glyph(face.face, glyph.index as FT_UInt, load_flags as FT_Int32) };
+            result = unsafe { FT_Load_Glyph(face.face, glyph.index() as FT_UInt, load_flags as FT_Int32) };
         };
 
         if succeeded(result) {
             let slot = unsafe { (*face.face).glyph };
             assert!(slot != ptr::null_mut());
 
             if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
                 unsafe { FT_GlyphSlot_Embolden(slot) };
@@ -351,17 +351,17 @@ impl FontContext {
                     debug!("format={:?}", format);
                     None
                 }
             }
         } else {
             error!("Unable to load glyph");
             debug!(
                 "{} of size {:?} from font {:?}, {:?}",
-                glyph.index,
+                glyph.index(),
                 font.size,
                 font.font_key,
                 result
             );
             None
         }
     }
 
--- a/gfx/webrender/src/platform/windows/font.rs
+++ b/gfx/webrender/src/platform/windows/font.rs
@@ -184,17 +184,17 @@ impl FontContext {
         &mut self,
         font: &FontInstance,
         key: &GlyphKey,
         size: f32,
         transform: Option<dwrote::DWRITE_MATRIX>,
         bitmaps: bool,
     ) -> dwrote::GlyphRunAnalysis {
         let face = self.get_font_face(font);
-        let glyph = key.index as u16;
+        let glyph = key.index() as u16;
         let advance = 0.0f32;
         let offset = dwrote::GlyphOffset {
             advanceOffset: 0.0,
             ascenderOffset: 0.0,
         };
 
         let glyph_run = dwrote::DWRITE_GLYPH_RUN {
             fontFace: unsafe { face.as_ptr() },
@@ -279,17 +279,17 @@ impl FontContext {
 
         // Alpha texture bounds can sometimes return an empty rect
         // Such as for spaces
         if width == 0 || height == 0 {
             return None;
         }
 
         let face = self.get_font_face(font);
-        face.get_design_glyph_metrics(&[key.index as u16], false)
+        face.get_design_glyph_metrics(&[key.index() as u16], false)
             .first()
             .map(|metrics| {
                 let em_size = size / 16.;
                 let design_units_per_pixel = face.metrics().designUnitsPerEm as f32 / 16. as f32;
                 let scaled_design_units_to_pixels = em_size / design_units_per_pixel;
                 let advance = metrics.advanceWidth as f32 * scaled_design_units_to_pixels;
 
                 GlyphDimensions {
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -2648,19 +2648,18 @@ impl PrimitiveStore {
                     run.count, pic_context.pipeline_id);
             }
             // TODO(gw): Perhaps we can restructure this to not need to create
             //           a new primitive context for every run (if the hash
             //           lookups ever show up in a profile).
             let scroll_node = &frame_context
                 .clip_scroll_tree
                 .spatial_nodes[run.clip_and_scroll.spatial_node_index.0];
-            let clip_chain = frame_context
-                .clip_scroll_tree
-                .get_clip_chain(run.clip_and_scroll.clip_chain_index);
+            let clip_chain = &frame_context
+                .clip_chains[run.clip_and_scroll.clip_chain_index.0];
 
             // Mark whether this picture contains any complex coordinate
             // systems, due to either the scroll node or the clip-chain.
             pic_state.has_non_root_coord_system |=
                 scroll_node.coordinate_system_id != CoordinateSystemId::root();
             pic_state.has_non_root_coord_system |= clip_chain.has_non_root_coord_system;
 
             if !scroll_node.invertible {
@@ -2751,30 +2750,37 @@ impl PrimitiveStore {
                         Some(ref chain_rect) => match prim_local_rect.intersection(chain_rect) {
                             Some(rect) => rect,
                             None => continue,
                         },
                         None => prim_local_rect,
                     };
 
                     if let Some(ref matrix) = parent_relative_transform {
-                        let bounds = matrix.transform_rect(&clipped_rect);
-                        result.local_rect_in_actual_parent_space =
-                            result.local_rect_in_actual_parent_space.union(&bounds);
+                        match matrix.transform_rect(&clipped_rect) {
+                            Some(bounds) => {
+                                result.local_rect_in_actual_parent_space =
+                                    result.local_rect_in_actual_parent_space.union(&bounds);
+                            }
+                            None => {
+                                warn!("parent relative transform can't transform the primitive rect for {:?}", prim_index);
+                            }
+                        }
+
                     }
                     if let Some(ref matrix) = original_relative_transform {
-                        let bounds = matrix.transform_rect(&clipped_rect);
-                        result.local_rect_in_original_parent_space =
-                            result.local_rect_in_original_parent_space.union(&bounds);
-                    }
-
-                    if let Some(ref matrix) = parent_relative_transform {
-                        let bounds = matrix.transform_rect(&prim_local_rect);
-                        result.local_rect_in_actual_parent_space =
-                            result.local_rect_in_actual_parent_space.union(&bounds);
+                        match matrix.transform_rect(&clipped_rect) {
+                            Some(bounds) => {
+                                result.local_rect_in_original_parent_space =
+                                    result.local_rect_in_original_parent_space.union(&bounds);
+                            }
+                            None => {
+                                warn!("original relative transform can't transform the primitive rect for {:?}", prim_index);
+                            }
+                        }
                     }
                 }
             }
         }
 
         result
     }
 }
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ApiMsg, BuiltDisplayList, ClearCache, DebugCommand};
 #[cfg(feature = "debugger")]
 use api::{BuiltDisplayListIter, SpecificDisplayItem};
 use api::{DeviceIntPoint, DevicePixelScale, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{DocumentId, DocumentLayer, ExternalScrollId, FrameMsg, HitTestFlags, HitTestResult};
 use api::{IdNamespace, LayoutPoint, PipelineId, RenderNotifier, SceneMsg, ScrollClamping};
-use api::{ScrollLocation, ScrollNodeState, TransactionMsg};
+use api::{ScrollLocation, ScrollNodeState, TransactionMsg, ResourceUpdate, ImageKey};
 use api::channel::{MsgReceiver, Payload};
 #[cfg(feature = "capture")]
 use api::CaptureBits;
 #[cfg(feature = "replay")]
 use api::CapturedDocument;
 use clip_scroll_tree::{SpatialNodeIndex, ClipScrollTree};
 #[cfg(feature = "debugger")]
 use debug_server;
@@ -221,20 +221,21 @@ impl Document {
 
         // Advance to the next frame.
         self.frame_id.0 += 1;
     }
 
     fn forward_transaction_to_scene_builder(
         &mut self,
         transaction_msg: TransactionMsg,
+        blobs_to_rasterize: &[ImageKey],
         document_ops: &DocumentOps,
         document_id: DocumentId,
         scene_id: u64,
-        resource_cache: &ResourceCache,
+        resource_cache: &mut ResourceCache,
         scene_tx: &Sender<SceneBuilderRequest>,
     ) {
         // Do as much of the error handling as possible here before dispatching to
         // the scene builder thread.
         let build_scene: bool = document_ops.build
             && self.pending.scene.root_pipeline_id.map(
                 |id| { self.pending.scene.pipelines.contains_key(&id) }
             ).unwrap_or(false);
@@ -247,18 +248,24 @@ impl Document {
                 font_instances: resource_cache.get_font_instances(),
                 output_pipelines: self.output_pipelines.clone(),
                 scene_id,
             })
         } else {
             None
         };
 
+        let (blob_rasterizer, blob_requests) = resource_cache.create_blob_scene_builder_requests(
+            blobs_to_rasterize
+        );
+
         scene_tx.send(SceneBuilderRequest::Transaction {
             scene: scene_request,
+            blob_requests,
+            blob_rasterizer,
             resource_updates: transaction_msg.resource_updates,
             frame_ops: transaction_msg.frame_ops,
             render: transaction_msg.generate_frame,
             document_id,
         }).unwrap();
     }
 
     fn render(
@@ -713,16 +720,18 @@ impl RenderBackend {
                 match msg {
                     SceneBuilderResult::Transaction {
                         document_id,
                         mut built_scene,
                         resource_updates,
                         frame_ops,
                         render,
                         result_tx,
+                        rasterized_blobs,
+                        blob_rasterizer,
                     } => {
                         let mut ops = DocumentOps::nop();
                         if let Some(doc) = self.documents.get_mut(&document_id) {
                             if let Some(mut built_scene) = built_scene.take() {
                                 doc.new_async_scene_ready(built_scene);
                                 // After applying the new scene we need to
                                 // rebuild the hit-tester, so we trigger a render
                                 // step.
@@ -750,20 +759,26 @@ impl RenderBackend {
                         let transaction_msg = TransactionMsg {
                             scene_ops: Vec::new(),
                             frame_ops,
                             resource_updates,
                             generate_frame: render,
                             use_scene_builder_thread: false,
                         };
 
+                        self.resource_cache.add_rasterized_blob_images(rasterized_blobs);
+                        if let Some(rasterizer) = blob_rasterizer {
+                            self.resource_cache.set_blob_rasterizer(rasterizer);
+                        }
+
                         if !transaction_msg.is_empty() || ops.render {
                             self.update_document(
                                 document_id,
                                 transaction_msg,
+                                &[],
                                 &mut frame_counter,
                                 &mut profile_counters,
                                 ops,
                                 true,
                             );
                         }
                     },
                     SceneBuilderResult::FlushComplete(tx) => {
@@ -820,19 +835,25 @@ impl RenderBackend {
         match msg {
             ApiMsg::WakeUp => {}
             ApiMsg::WakeSceneBuilder => {
                 self.scene_tx.send(SceneBuilderRequest::WakeUp).unwrap();
             }
             ApiMsg::FlushSceneBuilder(tx) => {
                 self.scene_tx.send(SceneBuilderRequest::Flush(tx)).unwrap();
             }
-            ApiMsg::UpdateResources(updates) => {
-                self.resource_cache
-                    .update_resources(updates, &mut profile_counters.resources);
+            ApiMsg::UpdateResources(mut updates) => {
+                self.resource_cache.pre_scene_building_update(
+                    &mut updates,
+                    &mut profile_counters.resources
+                );
+                self.resource_cache.post_scene_building_update(
+                    updates,
+                    &mut profile_counters.resources
+                );
             }
             ApiMsg::GetGlyphDimensions(instance_key, glyph_indices, tx) => {
                 let mut glyph_dimensions = Vec::with_capacity(glyph_indices.len());
                 if let Some(font) = self.resource_cache.get_font_instance(instance_key) {
                     for glyph_index in &glyph_indices {
                         let glyph_dim = self.resource_cache.get_glyph_dimensions(&font, *glyph_index);
                         glyph_dimensions.push(glyph_dim);
                     }
@@ -951,42 +972,55 @@ impl RenderBackend {
                     _ => ResultMsg::DebugCommand(option),
                 };
                 self.result_tx.send(msg).unwrap();
                 self.notifier.wake_up();
             }
             ApiMsg::ShutDown => {
                 return false;
             }
-            ApiMsg::UpdateDocument(document_id, doc_msgs) => {
+            ApiMsg::UpdateDocument(document_id, mut doc_msgs) => {
+                let blob_requests = get_blob_image_updates(&doc_msgs.resource_updates);
+
+                self.resource_cache.pre_scene_building_update(
+                    &mut doc_msgs.resource_updates,
+                    &mut profile_counters.resources,
+                );
+
                 self.update_document(
                     document_id,
                     doc_msgs,
+                    &blob_requests,
                     frame_counter,
                     profile_counters,
                     DocumentOps::nop(),
                     false,
                 )
             }
         }
 
         true
     }
 
     fn update_document(
         &mut self,
         document_id: DocumentId,
         mut transaction_msg: TransactionMsg,
+        blob_requests: &[ImageKey],
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
         initial_op: DocumentOps,
         has_built_scene: bool,
     ) {
         let mut op = initial_op;
 
+        if !blob_requests.is_empty() {
+            transaction_msg.use_scene_builder_thread = true;
+        }
+
         for scene_msg in transaction_msg.scene_ops.drain(..) {
             let _timer = profile_counters.total_time.timer();
             op.combine(
                 self.process_scene_msg(
                     document_id,
                     scene_msg,
                     *frame_counter,
                     &mut profile_counters.ipc,
@@ -995,27 +1029,28 @@ impl RenderBackend {
         }
 
         if transaction_msg.use_scene_builder_thread {
             let scene_id = self.make_unique_scene_id();
             let doc = self.documents.get_mut(&document_id).unwrap();
 
             doc.forward_transaction_to_scene_builder(
                 transaction_msg,
+                blob_requests,
                 &op,
                 document_id,
                 scene_id,
-                &self.resource_cache,
+                &mut self.resource_cache,
                 &self.scene_tx,
             );
 
             return;
         }
 
-        self.resource_cache.update_resources(
+        self.resource_cache.post_scene_building_update(
             transaction_msg.resource_updates,
             &mut profile_counters.resources,
         );
 
         if op.build {
             let scene_id = self.make_unique_scene_id();
             let doc = self.documents.get_mut(&document_id).unwrap();
             let _timer = profile_counters.total_time.timer();
@@ -1203,16 +1238,38 @@ impl RenderBackend {
 
             debug_root.add(builder.build());
         }
 
         serde_json::to_string(&debug_root).unwrap()
     }
 }
 
+fn get_blob_image_updates(updates: &[ResourceUpdate]) -> Vec<ImageKey> {
+    let mut requests = Vec::new();
+    for update in updates {
+        match *update {
+            ResourceUpdate::AddImage(ref img) => {
+                if img.data.is_blob() {
+                    requests.push(img.key);
+                }
+            }
+            ResourceUpdate::UpdateImage(ref img) => {
+                if img.data.is_blob() {
+                    requests.push(img.key);
+                }
+            }
+            _ => {}
+        }
+    }
+
+    requests
+}
+
+
 #[cfg(feature = "debugger")]
 trait ToDebugString {
     fn debug_string(&self) -> String;
 }
 
 #[cfg(feature = "debugger")]
 impl ToDebugString for SpecificDisplayItem {
     fn debug_string(&self) -> String {
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -4,17 +4,17 @@
 
 //! The webrender API.
 //!
 //! The `webrender::renderer` module provides the interface to webrender, which
 //! is accessible through [`Renderer`][renderer]
 //!
 //! [renderer]: struct.Renderer.html
 
-use api::{BlobImageRenderer, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
+use api::{BlobImageHandler, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentId, Epoch, ExternalImageId};
 use api::{ExternalImageType, FontRenderMode, FrameMsg, ImageFormat, PipelineId};
 use api::{RenderApiSender, RenderNotifier, TexelRect, TextureTarget};
 use api::{channel};
 use api::DebugCommand;
 use api::channel::PayloadReceiverHelperMethods;
 use batch::{BatchKind, BatchTextures, BrushBatchKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
@@ -1688,17 +1688,17 @@ impl Renderer {
                         }
                     })
                     .build();
                 Arc::new(worker.unwrap())
             });
         let sampler = options.sampler;
         let enable_render_on_scroll = options.enable_render_on_scroll;
 
-        let blob_image_renderer = options.blob_image_renderer.take();
+        let blob_image_handler = options.blob_image_handler.take();
         let thread_listener_for_render_backend = thread_listener.clone();
         let thread_listener_for_scene_builder = thread_listener.clone();
         let scene_builder_hooks = options.scene_builder_hooks;
         let rb_thread_name = format!("WRRenderBackend#{}", options.renderer_id.unwrap_or(0));
         let scene_thread_name = format!("WRSceneBuilder#{}", options.renderer_id.unwrap_or(0));
         let glyph_rasterizer = GlyphRasterizer::new(workers)?;
 
         let (scene_builder, scene_tx, scene_rx) = SceneBuilder::new(
@@ -1724,17 +1724,17 @@ impl Renderer {
             if let Some(ref thread_listener) = *thread_listener_for_render_backend {
                 thread_listener.thread_started(&rb_thread_name);
             }
 
             let texture_cache = TextureCache::new(max_device_size);
             let resource_cache = ResourceCache::new(
                 texture_cache,
                 glyph_rasterizer,
-                blob_image_renderer,
+                blob_image_handler,
             );
 
             let mut backend = RenderBackend::new(
                 api_rx,
                 payload_rx_for_backend,
                 result_tx,
                 scene_tx,
                 scene_rx,
@@ -4090,17 +4090,17 @@ pub struct RendererOptions {
     pub renderer_kind: RendererKind,
     pub enable_subpixel_aa: bool,
     pub clear_color: Option<ColorF>,
     pub enable_clear_scissor: bool,
     pub max_texture_size: Option<u32>,
     pub scatter_gpu_cache_updates: bool,
     pub upload_method: UploadMethod,
     pub workers: Option<Arc<ThreadPool>>,
-    pub blob_image_renderer: Option<Box<BlobImageRenderer>>,
+    pub blob_image_handler: Option<Box<BlobImageHandler>>,
     pub recorder: Option<Box<ApiRecordingReceiver>>,
     pub thread_listener: Option<Box<ThreadListener + Send + Sync>>,
     pub enable_render_on_scroll: bool,
     pub cached_programs: Option<Rc<ProgramCache>>,
     pub debug_flags: DebugFlags,
     pub renderer_id: Option<u64>,
     pub disable_dual_source_blending: bool,
     pub scene_builder_hooks: Option<Box<SceneBuilderHooks + Send>>,
@@ -4125,17 +4125,17 @@ impl Default for RendererOptions {
             enable_clear_scissor: true,
             max_texture_size: None,
             // Scattered GPU cache updates haven't met a test that would show their superiority yet.
             scatter_gpu_cache_updates: false,
             // This is best as `Immediate` on Angle, or `Pixelbuffer(Dynamic)` on GL,
             // but we are unable to make this decision here, so picking the reasonable medium.
             upload_method: UploadMethod::PixelBuffer(VertexUsageHint::Stream),
             workers: None,
-            blob_image_renderer: None,
+            blob_image_handler: None,
             recorder: None,
             thread_listener: None,
             enable_render_on_scroll: true,
             renderer_id: None,
             cached_programs: None,
             disable_dual_source_blending: false,
             scene_builder_hooks: None,
             sampler: None,
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -1,36 +1,37 @@
 /* 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::{AddFont, BlobImageResources, ResourceUpdate};
-use api::{BlobImageDescriptor, BlobImageError, BlobImageRenderer, BlobImageRequest};
+use api::{AddFont, BlobImageResources, AsyncBlobImageRasterizer, ResourceUpdate};
+use api::{BlobImageDescriptor, BlobImageHandler, BlobImageRequest};
 use api::{ClearCache, ColorF, DevicePoint, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{FontInstanceKey, FontKey, FontTemplate, GlyphIndex};
-use api::{ExternalImageData, ExternalImageType};
+use api::{ExternalImageData, ExternalImageType, BlobImageResult, BlobImageParams};
 use api::{FontInstanceOptions, FontInstancePlatformOptions, FontVariation};
 use api::{GlyphDimensions, IdNamespace};
 use api::{ImageData, ImageDescriptor, ImageKey, ImageRendering};
-use api::{TileOffset, TileSize};
+use api::{TileOffset, TileSize, TileRange, NormalizedRect, BlobImageData};
 use app_units::Au;
 #[cfg(feature = "capture")]
 use capture::ExternalCaptureImage;
 #[cfg(feature = "replay")]
 use capture::PlainExternalImage;
 #[cfg(any(feature = "replay", feature = "png"))]
 use capture::CaptureConfig;
 use device::TextureFilter;
-use euclid::size2;
+use euclid::{point2, size2};
 use glyph_cache::GlyphCache;
 #[cfg(not(feature = "pathfinder"))]
 use glyph_cache::GlyphCacheEntry;
 use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::UvRectKind;
+use image::{compute_tile_range, for_each_tile_in_range};
 use internal_types::{FastHashMap, FastHashSet, SourceTexture, TextureUpdateList};
 use profiler::{ResourceProfileCounters, TextureCacheProfileCounters};
 use render_backend::FrameId;
 use render_task::{RenderTaskCache, RenderTaskCacheKey, RenderTaskId};
 use render_task::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle, RenderTaskTree};
 use std::collections::hash_map::Entry::{self, Occupied, Vacant};
 use std::collections::hash_map::ValuesMut;
 use std::{cmp, mem};
@@ -90,21 +91,35 @@ pub struct ImageProperties {
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 enum State {
     Idle,
     AddResources,
     QueryResources,
 }
 
-#[derive(Debug)]
+/// Post scene building state.
+struct RasterizedBlobImage {
+    data: FastHashMap<Option<TileOffset>, BlobImageResult>,
+}
+
+/// Pre scene building state.
+/// We use this to generate the async blob rendering requests.
+struct BlobImageTemplate {
+    descriptor: ImageDescriptor,
+    tiling: Option<TileSize>,
+    dirty_rect: Option<DeviceUintRect>,
+    viewport_tiles: Option<TileRange>,
+}
+
 struct ImageResource {
     data: ImageData,
     descriptor: ImageDescriptor,
     tiling: Option<TileSize>,
+    viewport_tiles: Option<TileRange>,
 }
 
 #[derive(Clone, Debug)]
 pub struct ImageTiling {
     pub image_size: DeviceUintSize,
     pub tile_size: TileSize,
 }
 
@@ -355,37 +370,49 @@ pub struct ResourceCache {
     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.
     pending_image_requests: FastHashSet<ImageRequest>,
 
-    blob_image_renderer: Option<Box<BlobImageRenderer>>,
+    blob_image_handler: Option<Box<BlobImageHandler>>,
+    rasterized_blob_images: FastHashMap<ImageKey, RasterizedBlobImage>,
+    blob_image_templates: FastHashMap<ImageKey, BlobImageTemplate>,
+
+    // If while building a frame we encounter blobs that we didn't already
+    // rasterize, add them to this list and rasterize them synchronously.
+    missing_blob_images: Vec<BlobImageParams>,
+    // The rasterizer associated with the current scene.
+    blob_image_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
 }
 
 impl ResourceCache {
     pub fn new(
         texture_cache: TextureCache,
         glyph_rasterizer: GlyphRasterizer,
-        blob_image_renderer: Option<Box<BlobImageRenderer>>,
+        blob_image_handler: Option<Box<BlobImageHandler>>,
     ) -> Self {
         ResourceCache {
             cached_glyphs: GlyphCache::new(),
             cached_images: ResourceClassCache::new(),
             cached_render_tasks: RenderTaskCache::new(),
             resources: Resources::default(),
             cached_glyph_dimensions: FastHashMap::default(),
             texture_cache,
             state: State::Idle,
             current_frame_id: FrameId(0),
             pending_image_requests: FastHashSet::default(),
             glyph_rasterizer,
-            blob_image_renderer,
+            blob_image_handler,
+            rasterized_blob_images: FastHashMap::default(),
+            blob_image_templates: FastHashMap::default(),
+            missing_blob_images: Vec::new(),
+            blob_image_rasterizer: None,
         }
     }
 
     pub fn max_texture_size(&self) -> u32 {
         self.texture_cache.max_texture_size()
     }
 
     fn should_tile(limit: u32, descriptor: &ImageDescriptor, data: &ImageData) -> bool {
@@ -420,17 +447,17 @@ impl ResourceCache {
             gpu_cache,
             render_tasks,
             user_data,
             is_opaque,
             |render_task_tree| Ok(f(render_task_tree))
         ).expect("Failed to request a render task from the resource cache!")
     }
 
-    pub fn update_resources(
+    pub fn post_scene_building_update(
         &mut self,
         updates: Vec<ResourceUpdate>,
         profile_counters: &mut ResourceProfileCounters,
     ) {
         // TODO, there is potential for optimization here, by processing updates in
         // bulk rather than one by one (for example by sorting allocations by size or
         // in a way that reduces fragmentation in the atlas).
 
@@ -443,58 +470,132 @@ impl ResourceCache {
                     self.add_image_template(img.key, img.descriptor, img.data, img.tiling);
                 }
                 ResourceUpdate::UpdateImage(img) => {
                     self.update_image_template(img.key, img.descriptor, img.data, img.dirty_rect);
                 }
                 ResourceUpdate::DeleteImage(img) => {
                     self.delete_image_template(img);
                 }
-                ResourceUpdate::AddFont(font) => match font {
-                    AddFont::Raw(id, bytes, index) => {
-                        profile_counters.font_templates.inc(bytes.len());
-                        self.add_font_template(id, FontTemplate::Raw(Arc::new(bytes), index));
-                    }
-                    AddFont::Native(id, native_font_handle) => {
-                        self.add_font_template(id, FontTemplate::Native(native_font_handle));
-                    }
-                },
                 ResourceUpdate::DeleteFont(font) => {
                     self.delete_font_template(font);
                 }
-                ResourceUpdate::AddFontInstance(instance) => {
+                ResourceUpdate::DeleteFontInstance(font) => {
+                    self.delete_font_instance(font);
+                }
+                ResourceUpdate::SetImageVisibleArea(key, area) => {
+                    self.discard_tiles_outside_visible_area(key, &area);
+                }
+                ResourceUpdate::AddFont(_) |
+                ResourceUpdate::AddFontInstance(_) => {
+                    // Handled in update_resources_pre_scene_building
+                }
+            }
+        }
+    }
+
+    pub fn pre_scene_building_update(
+        &mut self,
+        updates: &mut Vec<ResourceUpdate>,
+        profile_counters: &mut ResourceProfileCounters,
+    ) {
+        let mut new_updates = Vec::with_capacity(updates.len());
+        for update in mem::replace(updates, Vec::new()) {
+            match update {
+                ResourceUpdate::AddImage(ref img) => {
+                    if let ImageData::Blob(ref blob_data) = img.data {
+                        self.add_blob_image(
+                            img.key,
+                            &img.descriptor,
+                            img.tiling,
+                            Arc::clone(blob_data),
+                        );
+                    }
+                }
+                ResourceUpdate::UpdateImage(ref img) => {
+                    if let ImageData::Blob(ref blob_data) = img.data {
+                        self.update_blob_image(
+                            img.key,
+                            &img.descriptor,
+                            &img.dirty_rect,
+                            Arc::clone(blob_data)
+                        );
+                    }
+                }
+                ResourceUpdate::SetImageVisibleArea(key, area) => {
+                    if let Some(template) = self.blob_image_templates.get_mut(&key) {
+                        if let Some(tile_size) = template.tiling {
+                            template.viewport_tiles = Some(compute_tile_range(
+                                &area,
+                                &template.descriptor.size,
+                                tile_size,
+                            ));
+                        }
+                    }
+                }
+                _ => {}
+            }
+
+            match update {
+                ResourceUpdate::AddFont(font) => {
+                    match font {
+                        AddFont::Raw(id, bytes, index) => {
+                            profile_counters.font_templates.inc(bytes.len());
+                            self.add_font_template(id, FontTemplate::Raw(Arc::new(bytes), index));
+                        }
+                        AddFont::Native(id, native_font_handle) => {
+                            self.add_font_template(id, FontTemplate::Native(native_font_handle));
+                        }
+                    }
+                }
+                ResourceUpdate::AddFontInstance(mut instance) => {
                     self.add_font_instance(
                         instance.key,
                         instance.font_key,
                         instance.glyph_size,
                         instance.options,
                         instance.platform_options,
                         instance.variations,
                     );
                 }
-                ResourceUpdate::DeleteFontInstance(instance) => {
-                    self.delete_font_instance(instance);
+                other => {
+                    new_updates.push(other);
                 }
             }
         }
+
+        *updates = new_updates;
+    }
+
+    pub fn set_blob_rasterizer(&mut self, rasterizer: Box<AsyncBlobImageRasterizer>) {
+        self.blob_image_rasterizer = Some(rasterizer);
+    }
+
+    pub fn add_rasterized_blob_images(&mut self, images: Vec<(BlobImageRequest, BlobImageResult)>) {
+        for (request, result) in images {
+            let image = self.rasterized_blob_images.entry(request.key).or_insert_with(
+                || { RasterizedBlobImage { data: FastHashMap::default() } }
+            );
+            image.data.insert(request.tile, result);
+        }
     }
 
     pub fn add_font_template(&mut self, font_key: FontKey, template: FontTemplate) {
         // Push the new font to the font renderer, and also store
         // it locally for glyph metric requests.
         self.glyph_rasterizer.add_font(font_key, template.clone());
         self.resources.font_templates.insert(font_key, template);
     }
 
     pub fn delete_font_template(&mut self, font_key: FontKey) {
         self.glyph_rasterizer.delete_font(font_key);
         self.resources.font_templates.remove(&font_key);
         self.cached_glyphs
             .clear_fonts(|font| font.font_key == font_key);
-        if let Some(ref mut r) = self.blob_image_renderer {
+        if let Some(ref mut r) = self.blob_image_handler {
             r.delete_font(font_key);
         }
     }
 
     pub fn add_font_instance(
         &mut self,
         instance_key: FontInstanceKey,
         font_key: FontKey,
@@ -527,17 +628,17 @@ impl ResourceCache {
             .insert(instance_key, instance);
     }
 
     pub fn delete_font_instance(&mut self, instance_key: FontInstanceKey) {
         self.resources.font_instances
             .write()
             .unwrap()
             .remove(&instance_key);
-        if let Some(ref mut r) = self.blob_image_renderer {
+        if let Some(ref mut r) = self.blob_image_handler {
             r.delete_font_instance(instance_key);
         }
     }
 
     pub fn get_font_instances(&self) -> FontInstanceMap {
         self.resources.font_instances.clone()
     }
 
@@ -554,28 +655,21 @@ impl ResourceCache {
         mut tiling: Option<TileSize>,
     ) {
         if tiling.is_none() && Self::should_tile(self.max_texture_size(), &descriptor, &data) {
             // We aren't going to be able to upload a texture this big, so tile it, even
             // if tiling was not requested.
             tiling = Some(DEFAULT_TILE_SIZE);
         }
 
-        if let ImageData::Blob(ref blob) = data {
-            self.blob_image_renderer.as_mut().unwrap().add(
-                image_key,
-                Arc::clone(&blob),
-                tiling,
-            );
-        }
-
         let resource = ImageResource {
             descriptor,
             data,
             tiling,
+            viewport_tiles: None,
         };
 
         self.resources.image_templates.insert(image_key, resource);
     }
 
     pub fn update_image_template(
         &mut self,
         image_key: ImageKey,
@@ -589,23 +683,16 @@ impl ResourceCache {
             None => panic!("Attempt to update non-existent image"),
         };
 
         let mut tiling = image.tiling;
         if tiling.is_none() && Self::should_tile(max_texture_size, &descriptor, &data) {
             tiling = Some(DEFAULT_TILE_SIZE);
         }
 
-        if let ImageData::Blob(ref blob) = data {
-            self.blob_image_renderer
-                .as_mut()
-                .unwrap()
-                .update(image_key, Arc::clone(blob), dirty_rect);
-        }
-
         // Each cache entry stores its own copy of the image's dirty rect. This allows them to be
         // updated independently.
         match self.cached_images.try_get_mut(&image_key) {
             Some(&mut ImageResult::UntiledAuto(ref mut entry)) => {
                 entry.dirty_rect = merge_dirty_rect(&entry.dirty_rect, &dirty_rect, &descriptor);
             }
             Some(&mut ImageResult::Multi(ref mut entries)) => {
                 for entry in entries.values_mut() {
@@ -614,27 +701,89 @@ impl ResourceCache {
             }
             _ => {}
         }
 
         *image = ImageResource {
             descriptor,
             data,
             tiling,
+            viewport_tiles: image.viewport_tiles,
+        };
+    }
+
+    // Happens before scene building.
+    pub fn add_blob_image(
+        &mut self,
+        key: ImageKey,
+        descriptor: &ImageDescriptor,
+        mut tiling: Option<TileSize>,
+        data: Arc<BlobImageData>,
+    ) {
+        let max_texture_size = self.max_texture_size();
+        tiling = get_blob_tiling(tiling, descriptor, max_texture_size);
+
+        self.blob_image_handler.as_mut().unwrap().add(key, data, tiling);
+
+        self.blob_image_templates.insert(
+            key,
+            BlobImageTemplate {
+                descriptor: *descriptor,
+                tiling,
+                dirty_rect: Some(
+                    DeviceUintRect::new(
+                        DeviceUintPoint::zero(),
+                        descriptor.size,
+                    )
+                ),
+                viewport_tiles: None,
+            },
+        );
+    }
+
+    // Happens before scene building.
+    pub fn update_blob_image(
+        &mut self,
+        key: ImageKey,
+        descriptor: &ImageDescriptor,
+        dirty_rect: &Option<DeviceUintRect>,
+        data: Arc<BlobImageData>,
+    ) {
+        self.blob_image_handler.as_mut().unwrap().update(key, data, *dirty_rect);
+
+        let max_texture_size = self.max_texture_size();
+
+        let image = self.blob_image_templates
+            .get_mut(&key)
+            .expect("Attempt to update non-existent blob image");
+
+        let tiling = get_blob_tiling(image.tiling, descriptor, max_texture_size);
+
+        *image = BlobImageTemplate {
+            descriptor: *descriptor,
+            tiling,
+            dirty_rect: match (*dirty_rect, image.dirty_rect) {
+                (Some(rect), Some(prev_rect)) => Some(rect.union(&prev_rect)),
+                (Some(rect), None) => Some(rect),
+                (None, _) => None,
+            },
+            viewport_tiles: image.viewport_tiles,
         };
     }
 
     pub fn delete_image_template(&mut self, image_key: ImageKey) {
         let value = self.resources.image_templates.remove(image_key);
 
         self.cached_images.remove(&image_key);
 
         match value {
             Some(image) => if image.data.is_blob() {
-                self.blob_image_renderer.as_mut().unwrap().delete(image_key);
+                self.blob_image_handler.as_mut().unwrap().delete(image_key);
+                self.blob_image_templates.remove(&image_key);
+                self.rasterized_blob_images.remove(&image_key);
             },
             None => {
                 warn!("Delete the non-exist key");
                 debug!("key={:?}", image_key);
             }
         }
     }
 
@@ -721,81 +870,224 @@ impl ResourceCache {
                     .or_insert(CachedImageInfo {
                         texture_cache_handle: TextureCacheHandle::new(),
                         dirty_rect: Some(template.descriptor.full_rect()),
                     })
             },
             ImageResult::Err(_) => panic!("Errors should already have been handled"),
         };
 
-        let needs_upload = self.texture_cache
-            .request(&entry.texture_cache_handle, gpu_cache);
+        self.texture_cache.request(&entry.texture_cache_handle, gpu_cache);
+
+        self.pending_image_requests.insert(request);
+
+        if template.data.is_blob() {
+            let request: BlobImageRequest = request.into();
+            let missing = match self.rasterized_blob_images.get(&request.key) {
+                Some(img) => !img.data.contains_key(&request.tile),
+                None => true,
+            };
 
-        let dirty_rect = if needs_upload {
-            // the texture cache entry has been evicted, treat it as all dirty
-            None
-        } else if entry.dirty_rect.is_none() {
-            return
-        } else {
-            entry.dirty_rect
-        };
+            // For some reason the blob image is missing. We'll fall back to
+            // rasterizing it on the render backend thread.
+            if missing {
+                let descriptor = match template.tiling {
+                    Some(tile_size) => {
+                        let tile = request.tile.unwrap();
+                        BlobImageDescriptor {
+                            offset: DevicePoint::new(
+                                tile.x as f32 * tile_size as f32,
+                                tile.y as f32 * tile_size as f32,
+                            ),
+                            size: compute_tile_size(
+                                &template.descriptor,
+                                tile_size,
+                                tile,
+                            ),
+                            format: template.descriptor.format,
+                        }
+                    }
+                    None => {
+                        BlobImageDescriptor {
+                            offset: DevicePoint::origin(),
+                            size: template.descriptor.size,
+                            format: template.descriptor.format,
+                        }
+                    }
+                };
 
-        if !self.pending_image_requests.insert(request) {
-            return
+                self.missing_blob_images.push(
+                    BlobImageParams {
+                        request,
+                        descriptor,
+                        dirty_rect: None,
+                    }
+                );
+            }
+        }
+    }
+
+    pub fn create_blob_scene_builder_requests(
+        &mut self,
+        keys: &[ImageKey]
+    ) -> (Option<Box<AsyncBlobImageRasterizer>>, Vec<BlobImageParams>) {
+        if self.blob_image_handler.is_none() {
+            return (None, Vec::new());
         }
 
-        // If we are tiling, then we need to confirm the dirty rect intersects
-        // the tile before leaving the request in the pending queue.
-        //
-        // We can start a worker thread rasterizing right now, if:
-        //  - The image is a blob.
-        //  - The blob hasn't already been requested this frame.
-        if template.data.is_blob() || dirty_rect.is_some() {
-            let (offset, size) = match request.tile {
-                Some(tile_offset) => {
-                    let tile_size = template.tiling.unwrap();
-                    let actual_size = compute_tile_size(
-                        &template.descriptor,
+        let mut blob_request_params = Vec::new();
+        for key in keys {
+            let template = self.blob_image_templates.get_mut(key).unwrap();
+
+            if let Some(tile_size) = template.tiling {
+                // If we know that only a portion of the blob image is in the viewport,
+                // only request these visible tiles since blob images can be huge.
+                let mut tiles = template.viewport_tiles.unwrap_or_else(|| {
+                    // Default to requesting the full range of tiles.
+                    compute_tile_range(
+                        &NormalizedRect {
+                            origin: point2(0.0, 0.0),
+                            size: size2(1.0, 1.0),
+                        },
+                        &template.descriptor.size,
                         tile_size,
-                        tile_offset,
+                    )
+                });
+
+                // Don't request tiles that weren't invalidated.
+                if let Some(dirty_rect) = template.dirty_rect {
+                    let f32_size = template.descriptor.size.to_f32();
+                    let normalized_dirty_rect = NormalizedRect {
+                        origin: point2(
+                            dirty_rect.origin.x as f32 / f32_size.width,
+                            dirty_rect.origin.y as f32 / f32_size.height,
+                        ),
+                        size: size2(
+                            dirty_rect.size.width as f32 / f32_size.width,
+                            dirty_rect.size.height as f32 / f32_size.height,
+                        ),
+                    };
+                    let dirty_tiles = compute_tile_range(
+                        &normalized_dirty_rect,
+                        &template.descriptor.size,
+                        tile_size,
                     );
 
-                    if let Some(dirty) = dirty_rect {
-                        if intersect_for_tile(dirty, actual_size, tile_size, tile_offset).is_none() {
-                            // don't bother requesting unchanged tiles
-                            entry.dirty_rect = None;
-                            self.pending_image_requests.remove(&request);
-                            return
+                    tiles = tiles.intersection(&dirty_tiles).unwrap_or(TileRange::zero());
+                }
+
+                // This code tries to keep things sane if Gecko sends
+                // nonsensical blob image requests.
+                // Constant here definitely needs to be tweaked.
+                const MAX_TILES_PER_REQUEST: u32 = 64;
+                while tiles.size.width as u32 * tiles.size.height as u32 > MAX_TILES_PER_REQUEST {
+                    // Remove tiles in the largest dimension.
+                    if tiles.size.width > tiles.size.height {
+                        tiles.size.width -= 2;
+                        tiles.origin.x += 1;
+                    } else {
+                        tiles.size.height -= 2;
+                        tiles.origin.y += 1;
+                    }
+                }
+
+                for_each_tile_in_range(&tiles, &mut|tile| {
+                    let descriptor = BlobImageDescriptor {
+                        offset: DevicePoint::new(
+                            tile.x as f32 * tile_size as f32,
+                            tile.y as f32 * tile_size as f32,
+                        ),
+                        size: compute_tile_size(
+                            &template.descriptor,
+                            tile_size,
+                            tile,
+                        ),
+                        format: template.descriptor.format,
+                    };
+
+                    blob_request_params.push(
+                        BlobImageParams {
+                            request: BlobImageRequest {
+                                key: *key,
+                                tile: Some(tile),
+                            },
+                            descriptor,
+                            dirty_rect: None,
                         }
-                    }
-
-                    let offset = DevicePoint::new(
-                        tile_offset.x as f32 * tile_size as f32,
-                        tile_offset.y as f32 * tile_size as f32,
                     );
-                    (offset, actual_size)
-                }
-                None => (DevicePoint::zero(), template.descriptor.size),
-            };
-
-            if template.data.is_blob() {
-                if let Some(ref mut renderer) = self.blob_image_renderer {
-                    renderer.request(
-                        &self.resources,
-                        request.into(),
-                        &BlobImageDescriptor {
-                            size,
-                            offset,
+                });
+            } else {
+                // TODO: to support partial rendering of non-tiled blobs we
+                // need to know that the current version of the blob is uploaded
+                // to the texture cache and get the guarantee that it will not
+                // get evicted by the time the updated blob is rasterized and
+                // uploaded.
+                // Alternatively we could make it the responsibility of the blob
+                // renderer to always output the full image. This could be based
+                // a similar copy-on-write mechanism as gecko tiling.
+                blob_request_params.push(
+                    BlobImageParams {
+                        request: BlobImageRequest {
+                            key: *key,
+                            tile: None,
+                        },
+                        descriptor: BlobImageDescriptor {
+                            offset: DevicePoint::zero(),
+                            size: template.descriptor.size,
                             format: template.descriptor.format,
                         },
-                        dirty_rect,
-                    );
+                        dirty_rect: None,
+                    }
+                );
+            }
+            template.dirty_rect = None;
+        }
+        let handler = self.blob_image_handler.as_mut().unwrap();
+        handler.prepare_resources(&self.resources, &blob_request_params);
+        (Some(handler.create_blob_rasterizer()), blob_request_params)
+    }
+
+    fn discard_tiles_outside_visible_area(
+        &mut self,
+        key: ImageKey,
+        area: &NormalizedRect
+    ) {
+        let template = match self.blob_image_templates.get(&key) {
+            Some(template) => template,
+            None => {
+                //println!("Missing image template (key={:?})!", key);
+                return;
+            }
+        };
+        let tile_size = match template.tiling {
+            Some(size) => size,
+            None => { return; }
+        };
+        let image = match self.rasterized_blob_images.get_mut(&key) {
+            Some(image) => image,
+            None => {
+                //println!("Missing rasterized blob (key={:?})!", key);
+                return;
+            }
+        };
+        let tile_range = compute_tile_range(
+            &area,
+            &template.descriptor.size,
+            tile_size,
+        );
+        image.data.retain(|tile, _| {
+            match *tile {
+                Some(offset) => tile_range.contains(&offset),
+                // This would be a bug. If we get here the blob should be tiled.
+                None => {
+                    error!("Blob image template and image data tiling don't match.");
+                    false
                 }
             }
-        }
+        });
     }
 
     pub fn request_glyphs(
         &mut self,
         mut font: FontInstance,
         glyph_keys: &[GlyphKey],
         gpu_cache: &mut GpuCache,
         render_task_tree: &mut RenderTaskTree,
@@ -1016,61 +1308,75 @@ impl ResourceCache {
             &mut self.cached_glyphs,
             &mut self.texture_cache,
             gpu_cache,
             &mut self.cached_render_tasks,
             render_tasks,
             texture_cache_profile,
         );
 
+        self.rasterize_missing_blob_images();
+
         // Apply any updates of new / updated images (incl. blobs) to the texture cache.
         self.update_texture_cache(gpu_cache);
         render_tasks.prepare_for_render();
         self.cached_render_tasks.update(
             gpu_cache,
             &mut self.texture_cache,
             render_tasks,
         );
         self.texture_cache.end_frame(texture_cache_profile);
     }
 
+    fn rasterize_missing_blob_images(&mut self) {
+        if self.missing_blob_images.is_empty() {
+            return;
+        }
+
+        self.blob_image_handler
+            .as_mut()
+            .unwrap()
+            .prepare_resources(&self.resources, &self.missing_blob_images);
+
+        let rasterized_blobs = self.blob_image_rasterizer
+            .as_mut()
+            .unwrap()
+            .rasterize(&self.missing_blob_images);
+
+        self.add_rasterized_blob_images(rasterized_blobs);
+
+        self.missing_blob_images.clear();
+    }
+
     fn update_texture_cache(&mut self, gpu_cache: &mut GpuCache) {
         for request in self.pending_image_requests.drain() {
             let image_template = self.resources.image_templates.get_mut(request.key).unwrap();
             debug_assert!(image_template.data.uses_texture_cache());
 
             let image_data = match image_template.data {
                 ImageData::Raw(..) | ImageData::External(..) => {
                     // Safe to clone here since the Raw image data is an
                     // Arc, and the external image data is small.
                     image_template.data.clone()
                 }
                 ImageData::Blob(..) => {
-                    // Extract the rasterized image from the blob renderer.
-                    match self.blob_image_renderer
-                        .as_mut()
-                        .unwrap()
-                        .resolve(request.into())
-                    {
-                        Ok(image) => ImageData::new(image.data),
-                        // TODO(nical): I think that we should handle these somewhat gracefully,
-                        // at least in the out-of-memory scenario.
-                        Err(BlobImageError::Oom) => {
-                            // This one should be recoverable-ish.
-                            panic!("Failed to render a vector image (OOM)");
+                    let blob_image = self.rasterized_blob_images.get(&request.key).unwrap();
+                    match blob_image.data.get(&request.tile) {
+                        Some(result) => {
+                            let result = result
+                                .as_ref()
+                                .expect("Failed to render a blob image");
+
+                            // TODO: we may want to not panic and show a placeholder instead.
+
+                            ImageData::Raw(Arc::clone(&result.data))
                         }
-                        Err(BlobImageError::InvalidKey) => {
-                            panic!("Invalid vector image key");
-                        }
-                        Err(BlobImageError::InvalidData) => {
-                            // TODO(nical): If we run into this we should kill the content process.
-                            panic!("Invalid vector image data");
-                        }
-                        Err(BlobImageError::Other(msg)) => {
-                            panic!("Vector image error {}", msg);
+                        None => {
+                            debug_assert!(false, "invalid blob image request during frame building");
+                            continue;
                         }
                     }
                 }
             };
 
             let entry = match *self.cached_images.get_mut(&request.key) {
                 ImageResult::UntiledAuto(ref mut entry) => entry,
                 ImageResult::Multi(ref mut entries) => entries.get_mut(&request.into()),
@@ -1192,22 +1498,37 @@ impl ResourceCache {
             self.glyph_rasterizer.delete_font(key);
         }
         self.resources
             .font_templates
             .retain(|key, _| key.0 != namespace);
         self.cached_glyphs
             .clear_fonts(|font| font.font_key.0 == namespace);
 
-        if let Some(ref mut r) = self.blob_image_renderer {
+        if let Some(ref mut r) = self.blob_image_handler {
             r.clear_namespace(namespace);
         }
     }
 }
 
+pub fn get_blob_tiling(
+    tiling: Option<TileSize>,
+    descriptor: &ImageDescriptor,
+    max_texture_size: u32,
+) -> Option<TileSize> {
+    if tiling.is_none() &&
+        (descriptor.size.width > max_texture_size ||
+         descriptor.size.height > max_texture_size) {
+        return Some(DEFAULT_TILE_SIZE);
+    }
+
+    tiling
+}
+
+
 // Compute the width and height of a tile depending on its position in the image.
 pub fn compute_tile_size(
     descriptor: &ImageDescriptor,
     base_size: TileSize,
     tile: TileOffset,
 ) -> DeviceUintSize {
     let base_size = base_size as u32;
     // Most tiles are going to have base_size as width and height,
@@ -1359,35 +1680,39 @@ impl ResourceCache {
                     fs::File::create(path_images.join(file_name))
                         .expect(&format!("Unable to create {}", short_path))
                         .write_all(&*arc)
                         .unwrap();
                     entry.insert(short_path);
                 }
                 ImageData::Blob(_) => {
                     assert_eq!(template.tiling, None);
-                    let request = BlobImageRequest {
-                        key,
-                        //TODO: support tiled blob images
-                        // https://github.com/servo/webrender/issues/2236
-                        tile: None,
-                    };
-                    let renderer = self.blob_image_renderer.as_mut().unwrap();
-                    renderer.request(
-                        &self.resources,
-                        request,
-                        &BlobImageDescriptor {
-                            size: desc.size,
-                            offset: DevicePoint::zero(),
-                            format: desc.format,
-                        },
-                        None,
-                    );
-                    let result = renderer.resolve(request)
-                        .expect("Blob resolve failed");
+                    let blob_request_params = &[
+                        BlobImageParams {
+                            request: BlobImageRequest {
+                                key,
+                                //TODO: support tiled blob images
+                                // https://github.com/servo/webrender/issues/2236
+                                tile: None,
+                            },
+                            descriptor: BlobImageDescriptor {
+                                size: desc.size,
+                                offset: DevicePoint::zero(),
+                                format: desc.format,
+                            },
+                            dirty_rect: None,
+                        }
+                    ];
+
+                    let blob_handler = self.blob_image_handler.as_mut().unwrap();
+                    blob_handler.prepare_resources(&self.resources, blob_request_params);
+                    let mut rasterizer = blob_handler.create_blob_rasterizer();
+                    let (_, result) = rasterizer.rasterize(blob_request_params).pop().unwrap();
+                    let result = result.expect("Blob rasterization failed");
+
                     assert_eq!(result.size, desc.size);
                     assert_eq!(result.data.len(), desc.compute_total_size() as usize);
 
                     num_blobs += 1;
                     #[cfg(feature = "png")]
                     CaptureConfig::save_png(
                         root.join(format!("blobs/{}.png", num_blobs)),
                         (desc.size.width, desc.size.height),
@@ -1562,14 +1887,15 @@ impl ResourceCache {
                     ImageData::Raw(arc)
                 }
             };
 
             res.image_templates.images.insert(key, ImageResource {
                 data,
                 descriptor: template.descriptor,
                 tiling: template.tiling,
+                viewport_tiles: None,
             });
         }
 
         external_images
     }
 }
--- a/gfx/webrender/src/scene_builder.rs
+++ b/gfx/webrender/src/scene_builder.rs
@@ -1,12 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+use api::{AsyncBlobImageRasterizer, BlobImageRequest, BlobImageParams, BlobImageResult};
 use api::{DocumentId, PipelineId, ApiMsg, FrameMsg, ResourceUpdate};
 use api::channel::MsgSender;
 use display_list_flattener::build_scene;
 use frame_builder::{FrameBuilderConfig, FrameBuilder};
 use clip_scroll_tree::ClipScrollTree;
 use internal_types::FastHashSet;
 use resource_cache::FontInstanceMap;
 use render_backend::DocumentView;
@@ -15,31 +16,35 @@ use scene::Scene;
 use std::sync::mpsc::{channel, Receiver, Sender};
 use time::precise_time_ns;
 
 // Message from render backend to scene builder.
 pub enum SceneBuilderRequest {
     Transaction {
         document_id: DocumentId,
         scene: Option<SceneRequest>,
+        blob_requests: Vec<BlobImageParams>,
+        blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
         resource_updates: Vec<ResourceUpdate>,
         frame_ops: Vec<FrameMsg>,
         render: bool,
     },
     WakeUp,
     Flush(MsgSender<()>),
     Stop
 }
 
 // Message from scene builder to render backend.
 pub enum SceneBuilderResult {
     Transaction {
         document_id: DocumentId,
         built_scene: Option<BuiltScene>,
         resource_updates: Vec<ResourceUpdate>,
+        rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+        blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
         frame_ops: Vec<FrameMsg>,
         render: bool,
         result_tx: Option<Sender<SceneSwapResult>>,
     },
     FlushComplete(MsgSender<()>),
     Stopped,
 }
 
@@ -129,26 +134,31 @@ impl SceneBuilder {
             SceneBuilderRequest::WakeUp => {}
             SceneBuilderRequest::Flush(tx) => {
                 self.tx.send(SceneBuilderResult::FlushComplete(tx)).unwrap();
                 let _ = self.api_tx.send(ApiMsg::WakeUp);
             }
             SceneBuilderRequest::Transaction {
                 document_id,
                 scene,
+                blob_requests,
+                mut blob_rasterizer,
                 resource_updates,
                 frame_ops,
                 render,
             } => {
                 let scenebuild_start_time = precise_time_ns();
                 let built_scene = scene.map(|request|{
                     build_scene(&self.config, request)
                 });
 
-                // TODO: pre-rasterization.
+                let rasterized_blobs = blob_rasterizer.as_mut().map_or(
+                    Vec::new(),
+                    |rasterizer| rasterizer.rasterize(&blob_requests),
+                );
 
                 // We only need the pipeline info and the result channel if we
                 // have a hook callback *and* if this transaction actually built
                 // a new scene that is going to get swapped in. In other cases
                 // pipeline_info can be None and we can avoid some overhead from
                 // invoking the hooks and blocking on the channel.
                 let (pipeline_info, result_tx, result_rx) = match (&self.hooks, &built_scene) {
                     (&Some(ref hooks), &Some(ref built)) => {
@@ -167,16 +177,18 @@ impl SceneBuilder {
                 };
 
                 let sceneswap_start_time = precise_time_ns();
                 let has_resources_updates = !resource_updates.is_empty();
                 self.tx.send(SceneBuilderResult::Transaction {
                     document_id,
                     built_scene,
                     resource_updates,
+                    rasterized_blobs,
+                    blob_rasterizer,
                     frame_ops,
                     render,
                     result_tx,
                 }).unwrap();
 
                 let _ = self.api_tx.send(ApiMsg::WakeUp);
 
                 if let Some(pipeline_info) = pipeline_info {
--- a/gfx/webrender/src/spatial_node.rs
+++ b/gfx/webrender/src/spatial_node.rs
@@ -28,25 +28,16 @@ pub enum SpatialNodeType {
     ReferenceFrame(ReferenceFrameInfo),
 
     /// An empty node, used to pad the ClipScrollTree's array of nodes so that
     /// we can immediately use each assigned SpatialNodeIndex. After display
     /// list flattening this node type should never be used.
     Empty,
 }
 
-impl SpatialNodeType {
-    fn is_reference_frame(&self) -> bool {
-        match *self {
-            SpatialNodeType::ReferenceFrame(_) => true,
-            _ => false,
-        }
-    }
-}
-
 /// Contains information common among all types of ClipScrollTree nodes.
 #[derive(Clone, Debug)]
 pub struct SpatialNode {
     /// The transformation for this viewport in world coordinates is the transformation for
     /// our parent reference frame, plus any accumulated scrolling offsets from nodes
     /// between our reference frame and this node. For reference frames, we also include
     /// whatever local transformation this reference frame provides.
     pub world_viewport_transform: LayoutToWorldFastTransform,
@@ -277,108 +268,92 @@ impl SpatialNode {
     }
 
     pub fn update_transform(
         &mut self,
         state: &mut TransformUpdateState,
         next_coordinate_system_id: &mut CoordinateSystemId,
         scene_properties: &SceneProperties,
     ) {
-        if self.node_type.is_reference_frame() {
-            self.update_transform_for_reference_frame(
-                state,
-                next_coordinate_system_id,
-                scene_properties
-            );
-            return;
-        }
-
-        // We calculate this here to avoid a double-borrow later.
-        let sticky_offset = self.calculate_sticky_offset(
-            &state.nearest_scrolling_ancestor_offset,
-            &state.nearest_scrolling_ancestor_viewport,
-        );
+        match self.node_type {
+            SpatialNodeType::ReferenceFrame(ref mut info) => {
+                // Resolve the transform against any property bindings.
+                let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
+                info.resolved_transform =
+                    LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
+                    .pre_mul(&source_transform.into())
+                    .pre_mul(&info.source_perspective);
 
-        // The transformation for the bounds of our viewport is the parent reference frame
-        // transform, plus any accumulated scroll offset from our parents, plus any offset
-        // provided by our own sticky positioning.
-        let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
-        self.world_viewport_transform = if accumulated_offset != LayoutVector2D::zero() {
-            state.parent_reference_frame_transform.pre_translate(&accumulated_offset)
-        } else {
-            state.parent_reference_frame_transform
-        };
+                // The transformation for this viewport in world coordinates is the transformation for
+                // our parent reference frame, plus any accumulated scrolling offsets from nodes
+                // between our reference frame and this node. Finally, we also include
+                // whatever local transformation this reference frame provides.
+                let relative_transform = info.resolved_transform
+                    .post_translate(state.parent_accumulated_scroll_offset)
+                    .to_transform()
+                    .with_destination::<LayoutPixel>();
+                self.world_viewport_transform =
+                    state.parent_reference_frame_transform.pre_mul(&relative_transform.into());
+                self.world_content_transform = self.world_viewport_transform;
 
-        // The transformation for any content inside of us is the viewport transformation, plus
-        // whatever scrolling offset we supply as well.
-        let scroll_offset = self.scroll_offset();
-        self.world_content_transform = if scroll_offset != LayoutVector2D::zero() {
-            self.world_viewport_transform.pre_translate(&scroll_offset)
-        } else {
-            self.world_viewport_transform
-        };
+                info.invertible = self.world_viewport_transform.is_invertible();
+                if !info.invertible {
+                    return;
+                }
 
-        let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
-        self.coordinate_system_relative_transform =
-            state.coordinate_system_relative_transform.offset(added_offset);
-
-        if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
-            info.current_offset = sticky_offset;
-        }
+                // Try to update our compatible coordinate system transform. If we cannot, start a new
+                // incompatible coordinate system.
+                match state.coordinate_system_relative_transform.update(relative_transform) {
+                    Some(offset) => self.coordinate_system_relative_transform = offset,
+                    None => {
+                        self.coordinate_system_relative_transform = LayoutFastTransform::identity();
+                        state.current_coordinate_system_id = *next_coordinate_system_id;
+                        next_coordinate_system_id.advance();
+                    }
+                }
 
-        self.coordinate_system_id = state.current_coordinate_system_id;
-    }
+                self.coordinate_system_id = state.current_coordinate_system_id;
+            }
+            _ => {
+                // We calculate this here to avoid a double-borrow later.
+                let sticky_offset = self.calculate_sticky_offset(
+                    &state.nearest_scrolling_ancestor_offset,
+                    &state.nearest_scrolling_ancestor_viewport,
+                );
 
-    pub fn update_transform_for_reference_frame(
-        &mut self,
-        state: &mut TransformUpdateState,
-        next_coordinate_system_id: &mut CoordinateSystemId,
-        scene_properties: &SceneProperties,
-    ) {
-        let info = match self.node_type {
-            SpatialNodeType::ReferenceFrame(ref mut info) => info,
-            _ => unreachable!("Called update_transform_for_reference_frame on non-ReferenceFrame"),
-        };
-
-        // Resolve the transform against any property bindings.
-        let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
-        info.resolved_transform =
-            LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
-            .pre_mul(&source_transform.into())
-            .pre_mul(&info.source_perspective);
+                // The transformation for the bounds of our viewport is the parent reference frame
+                // transform, plus any accumulated scroll offset from our parents, plus any offset
+                // provided by our own sticky positioning.
+                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
+                self.world_viewport_transform = if accumulated_offset != LayoutVector2D::zero() {
+                    state.parent_reference_frame_transform.pre_translate(&accumulated_offset)
+                } else {
+                    state.parent_reference_frame_transform
+                };
 
-        // The transformation for this viewport in world coordinates is the transformation for
-        // our parent reference frame, plus any accumulated scrolling offsets from nodes
-        // between our reference frame and this node. Finally, we also include
-        // whatever local transformation this reference frame provides.
-        let relative_transform = info.resolved_transform
-            .post_translate(state.parent_accumulated_scroll_offset)
-            .to_transform()
-            .with_destination::<LayoutPixel>();
-        self.world_viewport_transform =
-            state.parent_reference_frame_transform.pre_mul(&relative_transform.into());
-        self.world_content_transform = self.world_viewport_transform;
+                // The transformation for any content inside of us is the viewport transformation, plus
+                // whatever scrolling offset we supply as well.
+                let scroll_offset = self.scroll_offset();
+                self.world_content_transform = if scroll_offset != LayoutVector2D::zero() {
+                    self.world_viewport_transform.pre_translate(&scroll_offset)
+                } else {
+                    self.world_viewport_transform
+                };
 
-        info.invertible = self.world_viewport_transform.is_invertible();
-        if !info.invertible {
-            return;
-        }
+                let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
+                self.coordinate_system_relative_transform =
+                    state.coordinate_system_relative_transform.offset(added_offset);
 
-        // Try to update our compatible coordinate system transform. If we cannot, start a new
-        // incompatible coordinate system.
-        match state.coordinate_system_relative_transform.update(relative_transform) {
-            Some(offset) => self.coordinate_system_relative_transform = offset,
-            None => {
-                self.coordinate_system_relative_transform = LayoutFastTransform::identity();
-                state.current_coordinate_system_id = *next_coordinate_system_id;
-                next_coordinate_system_id.advance();
+                if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
+                    info.current_offset = sticky_offset;
+                }
+
+                self.coordinate_system_id = state.current_coordinate_system_id;
             }
         }
-
-        self.coordinate_system_id = state.current_coordinate_system_id;
     }
 
     fn calculate_sticky_offset(
         &self,
         viewport_scroll_offset: &LayoutVector2D,
         viewport_rect: &LayoutRect,
     ) -> LayoutVector2D {
         let info = match self.node_type {
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale, DeviceUintPoint};
 use api::{DeviceUintRect, DeviceUintSize, DocumentLayer, FilterOp, ImageFormat, LayoutRect};
 use api::{MixBlendMode, PipelineId};
 use batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image};
 use clip::{ClipStore};
-use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
+use clip_scroll_tree::SpatialNodeIndex;
 use device::{FrameId, Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use gpu_cache::{GpuCache};
 use gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, TransformData, TransformPalette};
 use internal_types::{FastHashMap, SavedTargetIndex, SourceTexture};
 #[cfg(feature = "pathfinder")]
 use pathfinder_partitioner::mesh::Mesh;
@@ -40,17 +40,16 @@ pub struct ScrollbarPrimitive {
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct RenderTargetIndex(pub usize);
 
 pub struct RenderTargetContext<'a, 'rc> {
     pub device_pixel_scale: DevicePixelScale,
     pub prim_store: &'a PrimitiveStore,
     pub resource_cache: &'rc mut ResourceCache,
-    pub clip_scroll_tree: &'a ClipScrollTree,
     pub use_dual_source_blending: bool,
     pub transforms: &'a TransformPalette,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct TextureAllocator {
     // TODO(gw): Replace this with a simpler allocator for
--- a/gfx/webrender/src/util.rs
+++ b/gfx/webrender/src/util.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::{BorderRadius, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale};
 use api::{DevicePoint, DeviceRect, DeviceSize, LayoutPixel, LayoutPoint, LayoutRect, LayoutSize};
-use api::{WorldPixel, WorldRect};
+use api::{WorldPixel, WorldPoint, WorldRect};
 use euclid::{Point2D, Rect, Size2D, TypedPoint2D, TypedRect, TypedSize2D};
 use euclid::{TypedTransform2D, TypedTransform3D, TypedVector2D, TypedVector3D};
 use euclid::{HomogeneousVector};
 use num_traits::Zero;
 use plane_split::{Clipper, Plane, Polygon};
 use std::{i32, f32};
 
 // Matches the definition of SK_ScalarNearlyZero in Skia.
@@ -257,25 +257,29 @@ pub fn calculate_screen_bounding_rect(
             .into_iter()
             // filter out parts behind the view plane
             .flat_map(|poly| &poly.points)
             .map(|p| {
                 debug!("\tpoint {:?} -> {:?} -> {:?}", p,
                     transform.transform_point2d_homogeneous(&p.to_2d()),
                     transform.transform_point2d(&p.to_2d())
                 );
-                transform.transform_point2d(&p.to_2d())
+                //TODO: change to `expect` when the near splitting code is ready
+                transform
+                    .transform_point2d(&p.to_2d())
+                    .unwrap_or(WorldPoint::zero())
             })
         )
     } else {
+        // we just checked for all the points to be in positive hemisphere, so `unwrap` is valid
         WorldRect::from_points(&[
-            homogens[0].to_point2d(),
-            homogens[1].to_point2d(),
-            homogens[2].to_point2d(),
-            homogens[3].to_point2d(),
+            homogens[0].to_point2d().unwrap(),
+            homogens[1].to_point2d().unwrap(),
+            homogens[2].to_point2d().unwrap(),
+            homogens[3].to_point2d().unwrap(),
         ])
     };
 
     debug!("world rect {:?}", world_rect);
     (world_rect * device_pixel_scale)
         .round_out()
         .intersection(&max_rect)
         .map(|r| r.to_i32())
@@ -545,21 +549,21 @@ impl<Src, Dst> FastTransform<Src, Dst> {
     pub fn is_backface_visible(&self) -> bool {
         match *self {
             FastTransform::Offset(..) => false,
             FastTransform::Transform { ref transform, .. } => transform.is_backface_visible(),
         }
     }
 
     #[inline(always)]
-    pub fn transform_point2d(&self, point: &TypedPoint2D<f32, Src>) -> TypedPoint2D<f32, Dst> {
+    pub fn transform_point2d(&self, point: &TypedPoint2D<f32, Src>) -> Option<TypedPoint2D<f32, Dst>> {
         match *self {
             FastTransform::Offset(offset) => {
                 let new_point = *point + offset;
-                TypedPoint2D::from_untyped(&new_point.to_untyped())
+                Some(TypedPoint2D::from_untyped(&new_point.to_untyped()))
             }
             FastTransform::Transform { ref transform, .. } => transform.transform_point2d(point),
         }
     }
 
     #[inline(always)]
     pub fn transform_point2d_homogeneous(&self, point: &TypedPoint2D<f32, Src>) -> HomogeneousVector<f32, Dst> {
         match *self {
@@ -567,30 +571,30 @@ impl<Src, Dst> FastTransform<Src, Dst> {
                 let new_point = *point + offset;
                 HomogeneousVector::new(new_point.x, new_point.y, 0.0, 1.0)
             }
             FastTransform::Transform { ref transform, .. } => transform.transform_point2d_homogeneous(point),
         }
     }
 
     #[inline(always)]
-    pub fn transform_rect(&self, rect: &TypedRect<f32, Src>) -> TypedRect<f32, Dst> {
+    pub fn transform_rect(&self, rect: &TypedRect<f32, Src>) -> Option<TypedRect<f32, Dst>> {
         match *self {
             FastTransform::Offset(offset) =>
-                TypedRect::from_untyped(&rect.to_untyped().translate(&offset.to_untyped())),
+                Some(TypedRect::from_untyped(&rect.to_untyped().translate(&offset.to_untyped()))),
             FastTransform::Transform { ref transform, .. } => transform.transform_rect(rect),
         }
     }
 
     pub fn unapply(&self, rect: &TypedRect<f32, Dst>) -> Option<TypedRect<f32, Src>> {
         match *self {
             FastTransform::Offset(offset) =>
                 Some(TypedRect::from_untyped(&rect.to_untyped().translate(&-offset.to_untyped()))),
             FastTransform::Transform { inverse: Some(ref inverse), is_2d: true, .. }  =>
-                Some(inverse.transform_rect(rect)),
+                inverse.transform_rect(rect),
             FastTransform::Transform { ref transform, is_2d: false, .. } =>
                 Some(transform.inverse_rect_footprint(rect)),
             FastTransform::Transform { inverse: None, .. }  => None,
         }
     }
 
     #[inline(always)]
     pub fn offset(&self, new_offset: TypedVector2D<f32, Src>) -> Self {
--- a/gfx/webrender_api/Cargo.toml
+++ b/gfx/webrender_api/Cargo.toml
@@ -12,17 +12,17 @@ serialize = []
 deserialize = []
 
 [dependencies]
 app_units = "0.6"
 bincode = "1.0"
 bitflags = "1.0"
 byteorder = "1.2.1"
 ipc-channel = {version = "0.10.0", optional = true}
-euclid = { version = "0.17", features = ["serde"] }
+euclid = { version = "0.18", features = ["serde"] }
 serde = { version = "=1.0.66", features = ["rc"] }
 serde_derive = { version = "=1.0.66", features = ["deserialize_in_place"] }
 serde_bytes = "0.10"
 time = "0.1"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation = "0.6"
 core-graphics = "0.14"
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -10,27 +10,28 @@ use std::cell::Cell;
 use std::fmt;
 use std::marker::PhantomData;
 use std::path::PathBuf;
 use std::u32;
 use {BuiltDisplayList, BuiltDisplayListDescriptor, ColorF, DeviceIntPoint, DeviceUintRect};
 use {DeviceUintSize, ExternalScrollId, FontInstanceKey, FontInstanceOptions};
 use {FontInstancePlatformOptions, FontKey, FontVariation, GlyphDimensions, GlyphIndex, ImageData};
 use {ImageDescriptor, ImageKey, ItemTag, LayoutPoint, LayoutSize, LayoutTransform, LayoutVector2D};
-use {NativeFontHandle, WorldPoint};
+use {NativeFontHandle, WorldPoint, NormalizedRect};
 
 pub type TileSize = u16;
 /// Documents are rendered in the ascending order of their associated layer values.
 pub type DocumentLayer = i8;
 
 #[derive(Clone, Deserialize, Serialize)]
 pub enum ResourceUpdate {
     AddImage(AddImage),
     UpdateImage(UpdateImage),
     DeleteImage(ImageKey),
+    SetImageVisibleArea(ImageKey, NormalizedRect),
     AddFont(AddFont),
     DeleteFont(FontKey),
     AddFontInstance(AddFontInstance),
     DeleteFontInstance(FontInstanceKey),
 }
 
 /// A Transaction is a group of commands to apply atomically to a document.
 ///
@@ -289,16 +290,20 @@ impl Transaction {
             dirty_rect,
         }));
     }
 
     pub fn delete_image(&mut self, key: ImageKey) {
         self.resource_updates.push(ResourceUpdate::DeleteImage(key));
     }
 
+    pub fn set_image_visible_area(&mut self, key: ImageKey, area: NormalizedRect) {
+        self.resource_updates.push(ResourceUpdate::SetImageVisibleArea(key, area))
+    }
+
     pub fn add_raw_font(&mut self, key: FontKey, bytes: Vec<u8>, index: u32) {
         self.resource_updates
             .push(ResourceUpdate::AddFont(AddFont::Raw(key, bytes, index)));
     }
 
     pub fn add_native_font(&mut self, key: FontKey, native_handle: NativeFontHandle) {
         self.resource_updates
             .push(ResourceUpdate::AddFont(AddFont::Native(key, native_handle)));
--- a/gfx/webrender_api/src/image.rs
+++ b/gfx/webrender_api/src/image.rs
@@ -169,67 +169,96 @@ impl ImageData {
                 ExternalImageType::Buffer => true,
             },
             ImageData::Blob(_) => true,
             ImageData::Raw(_) => true,
         }
     }
 }
 
+/// The resources exposed by the resource cache available for use by the blob rasterizer.
 pub trait BlobImageResources {
     fn get_font_data(&self, key: FontKey) -> &FontTemplate;
     fn get_image(&self, key: ImageKey) -> Option<(&ImageData, &ImageDescriptor)>;
 }
 
-pub trait BlobImageRenderer: Send {
+/// A handler on the render backend that can create rasterizer objects which will
+/// be sent to the scene builder thread to execute the rasterization.
+///
+/// The handler is responsible for collecting resources, managing/updating blob commands
+/// and creating the rasterizer objects, but isn't expected to do any rasterization itself.
+pub trait BlobImageHandler: Send {
+    /// Creates a snapshot of the current state of blob images in the handler.
+    fn create_blob_rasterizer(&mut self) -> Box<AsyncBlobImageRasterizer>;
+
+    /// A hook to let the blob image handler update any state related to resources that
+    /// are not bundled in the blob recording itself.
+    fn prepare_resources(
+        &mut self,
+        services: &BlobImageResources,
+        requests: &[BlobImageParams],
+    );
+
+    /// Register a blob image.
     fn add(&mut self, key: ImageKey, data: Arc<BlobImageData>, tiling: Option<TileSize>);
 
+    /// Update an already registered blob image.
     fn update(&mut self, key: ImageKey, data: Arc<BlobImageData>, dirty_rect: Option<DeviceUintRect>);
 
+    /// Delete an already registered blob image.
     fn delete(&mut self, key: ImageKey);
 
-    fn request(
-        &mut self,
-        resources: &BlobImageResources,
-        key: BlobImageRequest,
-        descriptor: &BlobImageDescriptor,
-        dirty_rect: Option<DeviceUintRect>,
-    );
-
-    fn resolve(&mut self, key: BlobImageRequest) -> BlobImageResult;
-
+    /// A hook to let the handler clean up any state related to a font which the resource
+    /// cache is about to delete.
     fn delete_font(&mut self, key: FontKey);
 
+    /// A hook to let the handler clean up any state related to a font instance which the
+    /// resource cache is about to delete.
     fn delete_font_instance(&mut self, key: FontInstanceKey);
 
+    /// A hook to let the handler clean up any state related a given namespace before the
+    /// resource cache deletes them.
     fn clear_namespace(&mut self, namespace: IdNamespace);
 }
 
+/// A group of rasterization requests to execute synchronously on the scene builder thread.
+pub trait AsyncBlobImageRasterizer : Send {
+    fn rasterize(&mut self, requests: &[BlobImageParams]) -> Vec<(BlobImageRequest, BlobImageResult)>;
+}
+
+
+#[derive(Copy, Clone, Debug)]
+pub struct BlobImageParams {
+    pub request: BlobImageRequest,
+    pub descriptor: BlobImageDescriptor,
+    pub dirty_rect: Option<DeviceUintRect>,
+}
+
 pub type BlobImageData = Vec<u8>;
 
 pub type BlobImageResult = Result<RasterizedBlobImage, BlobImageError>;
 
 #[repr(C)]
 #[derive(Copy, Clone, Debug)]
 pub struct BlobImageDescriptor {
     pub size: DeviceUintSize,
     pub offset: DevicePoint,
     pub format: ImageFormat,
 }
 
 pub struct RasterizedBlobImage {
     pub size: DeviceUintSize,
-    pub data: Vec<u8>,
+    pub data: Arc<Vec<u8>>,
 }
 
 #[derive(Clone, Debug)]
 pub enum BlobImageError {
     Oom,
     InvalidKey,
     InvalidData,
     Other(String),
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct BlobImageRequest {
     pub key: ImageKey,
     pub tile: Option<TileOffset>,
 }
--- a/gfx/webrender_api/src/units.rs
+++ b/gfx/webrender_api/src/units.rs
@@ -81,16 +81,17 @@ pub type WorldSize = TypedSize2D<f32, Wo
 pub type WorldPoint3D = TypedPoint3D<f32, WorldPixel>;
 pub type WorldVector2D = TypedVector2D<f32, WorldPixel>;
 pub type WorldVector3D = TypedVector3D<f32, WorldPixel>;
 
 /// Offset in number of tiles.
 #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub struct Tiles;
 pub type TileOffset = TypedPoint2D<u16, Tiles>;
+pub type TileRange = TypedRect<u16, Tiles>;
 
 /// Scaling ratio from world pixels to device pixels.
 pub type DevicePixelScale = TypedScale<f32, WorldPixel, DevicePixel>;
 /// Scaling ratio from layout to world. Used for cases where we know the layout
 /// is in world space, or specifically want to treat it this way.
 pub type LayoutToWorldScale = TypedScale<f32, LayoutPixel, WorldPixel>;
 /// A complete scaling ratio from layout space to device pixel space.
 pub type LayoutToDeviceScale = TypedScale<f32, LayoutPixel, DevicePixel>;
@@ -110,16 +111,22 @@ pub type LayoutSizeAu = TypedSize2D<Au, 
 pub fn as_scroll_parent_rect(rect: &LayoutRect) -> ScrollLayerRect {
     ScrollLayerRect::from_untyped(&rect.to_untyped())
 }
 
 pub fn as_scroll_parent_vector(vector: &LayoutVector2D) -> ScrollLayerVector2D {
     ScrollLayerVector2D::from_untyped(&vector.to_untyped())
 }
 
+/// Coordinates in normalized space (between zero and one).
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct NormalizedCoordinates;
+
+pub type NormalizedRect = TypedRect<f32, NormalizedCoordinates>;
+
 /// Stores two coordinates in texel space. The coordinates
 /// are stored in texel coordinates because the texture atlas
 /// may grow. Storing them as texel coords and normalizing
 /// the UVs in the vertex shader means nothing needs to be
 /// updated on the CPU when the texture size changes.
 #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
 pub struct TexelRect {
     pub uv0: DevicePoint,
--- a/gfx/webrender_bindings/Cargo.toml
+++ b/gfx/webrender_bindings/Cargo.toml
@@ -2,19 +2,19 @@
 name = "webrender_bindings"
 version = "0.1.0"
 authors = ["The Mozilla Project Developers"]
 license = "MPL-2.0"
 
 [dependencies]
 rayon = "1"
 thread_profiler = "0.1.1"
-euclid = { version = "0.17", features = ["serde"] }
+euclid = { version = "0.18", features = ["serde"] }
 app_units = "0.6"
-gleam = "0.5"
+gleam = "0.6"
 log = "0.4"
 nsstring = { path = "../../servo/support/gecko/nsstring" }
 bincode = "1.0"
 uuid = {version = "0.1.18"}
 fxhash = "0.2.1"
 
 [dependencies.webrender]
 path = "../webrender"
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-88dab3f611b05516c1c54a7cb35813b796b08584
+9f21ee5dba0694818a1e2e46d95734ede281447c
--- a/gfx/wrench/Cargo.toml
+++ b/gfx/wrench/Cargo.toml
@@ -5,18 +5,18 @@ authors = ["Vladimir Vukicevic <vladimir
 build = "build.rs"
 license = "MPL-2.0"
 
 [dependencies]
 base64 = "0.6"
 bincode = "1.0"
 byteorder = "1.0"
 env_logger = { version = "0.5", optional = true }
-euclid = "0.17"
-gleam = "0.5"
+euclid = "0.18"
+gleam = "0.6"
 glutin = "0.17"
 app_units = "0.6"
 image = "0.19"
 clap = { version = "2", features = ["yaml"] }
 lazy_static = "1"
 log = "0.4"
 yaml-rust = { git = "https://github.com/vvuk/yaml-rust", features = ["preserve_order"] }
 serde_json = "1.0"
--- a/gfx/wrench/src/angle.rs
+++ b/gfx/wrench/src/angle.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 glutin::{self, ContextBuilder, CreationError};
 #[cfg(not(windows))]
-use glutin::dpi::PhysicalSize;
+use winit::dpi::PhysicalSize;
 use winit::{EventsLoop, Window, WindowBuilder};
 
 #[cfg(not(windows))]
 pub enum Context {}
 
 #[cfg(windows)]
 pub use ::egl::Context;
 
--- a/gfx/wrench/src/blob.rs
+++ b/gfx/wrench/src/blob.rs
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// A very basic BlobImageRenderer that can only render a checkerboard pattern.
+// A very basic BlobImageRasterizer that can only render a checkerboard pattern.
 
 use std::collections::HashMap;
 use std::sync::Arc;
 use std::sync::Mutex;
 use webrender::api::*;
 use webrender::intersect_for_tile;
 use euclid::size2;
 
@@ -60,17 +60,16 @@ fn render_blob(
     ));
 
     if let Some((tile_size, tile)) = tile {
         dirty_rect = intersect_for_tile(dirty_rect, size2(tile_size as u32, tile_size as u32),
                                         tile_size, tile)
             .expect("empty rects should be culled by webrender");
     }
 
-
     for y in dirty_rect.min_y() .. dirty_rect.max_y() {
         for x in dirty_rect.min_x() .. dirty_rect.max_x() {
             // Apply the tile's offset. This is important: all drawing commands should be
             // translated by this offset to give correct results with tiled blob images.
             let x2 = x + descriptor.offset.x as u32;
             let y2 = y + descriptor.offset.y as u32;
 
             // Render a simple checkerboard pattern
@@ -98,92 +97,111 @@ fn render_blob(
                         format!("Unsupported image format {:?}", descriptor.format),
                     ));
                 }
             }
         }
     }
 
     Ok(RasterizedBlobImage {
-        data: texels,
+        data: Arc::new(texels),
         size: descriptor.size,
     })
 }
 
+/// See rawtest.rs. We use this to test that blob images are requested the right
+/// amount of times.
 pub struct BlobCallbacks {
-    pub request: Box<Fn(&BlobImageRequest) + Send + 'static>,
-    pub resolve: Box<Fn() + Send + 'static>,
+    pub request: Box<Fn(&[BlobImageParams]) + Send + 'static>,
 }
 
 impl BlobCallbacks {
     pub fn new() -> Self {
-        BlobCallbacks { request: Box::new(|_|()), resolve: Box::new(|| (())) }
+        BlobCallbacks { request: Box::new(|_|()) }
     }
 }
 
 pub struct CheckerboardRenderer {
     image_cmds: HashMap<ImageKey, (ColorU, Option<TileSize>)>,
     callbacks: Arc<Mutex<BlobCallbacks>>,
-
-    // The images rendered in the current frame (not kept here between frames).
-    rendered_images: HashMap<BlobImageRequest, BlobImageResult>,
 }
 
 impl CheckerboardRenderer {
     pub fn new(callbacks: Arc<Mutex<BlobCallbacks>>) -> Self {
         CheckerboardRenderer {
             callbacks,
             image_cmds: HashMap::new(),
-            rendered_images: HashMap::new(),
         }
     }
 }
 
-impl BlobImageRenderer for CheckerboardRenderer {
+impl BlobImageHandler for CheckerboardRenderer {
     fn add(&mut self, key: ImageKey, cmds: Arc<BlobImageData>, tile_size: Option<TileSize>) {
         self.image_cmds
             .insert(key, (deserialize_blob(&cmds[..]).unwrap(), tile_size));
     }
 
     fn update(&mut self, key: ImageKey, cmds: Arc<BlobImageData>, _dirty_rect: Option<DeviceUintRect>) {
         // Here, updating is just replacing the current version of the commands with
         // the new one (no incremental updates).
         self.image_cmds.get_mut(&key).unwrap().0 = deserialize_blob(&cmds[..]).unwrap();
     }
 
     fn delete(&mut self, key: ImageKey) {
         self.image_cmds.remove(&key);
     }
 
-    fn request(
-        &mut self,
-        _resources: &BlobImageResources,
-        request: BlobImageRequest,
-        descriptor: &BlobImageDescriptor,
-        dirty_rect: Option<DeviceUintRect>,
-    ) {
-        (self.callbacks.lock().unwrap().request)(&request);
-        assert!(!self.rendered_images.contains_key(&request));
-        // This method is where we kick off our rendering jobs.
-        // It should avoid doing work on the calling thread as much as possible.
-        // In this example we will use the thread pool to render individual tiles.
-
-        // Gather the input data to send to a worker thread.
-        let &(color, tile_size) = self.image_cmds.get(&request.key).unwrap();
-
-        let tile = request.tile.map(|tile| (tile_size.unwrap(), tile));
-
-        let result = render_blob(color, descriptor, tile, dirty_rect);
-
-        self.rendered_images.insert(request, result);
-    }
-
-    fn resolve(&mut self, request: BlobImageRequest) -> BlobImageResult {
-        (self.callbacks.lock().unwrap().resolve)();
-        self.rendered_images.remove(&request).unwrap()
-    }
-
     fn delete_font(&mut self, _key: FontKey) {}
 
     fn delete_font_instance(&mut self, _key: FontInstanceKey) {}
 
     fn clear_namespace(&mut self, _namespace: IdNamespace) {}
+
+    fn prepare_resources(
+        &mut self,
+        _services: &BlobImageResources,
+        requests: &[BlobImageParams],
+    ) {
+        if !requests.is_empty() {
+            (self.callbacks.lock().unwrap().request)(&requests);
+        }
+    }
+
+    fn create_blob_rasterizer(&mut self) -> Box<AsyncBlobImageRasterizer> {
+        Box::new(Rasterizer { image_cmds: self.image_cmds.clone() })
+    }
 }
+
+struct Command {
+    request: BlobImageRequest,
+    color: ColorU,
+    descriptor: BlobImageDescriptor,
+    tile: Option<(TileSize, TileOffset)>,
+    dirty_rect: Option<DeviceUintRect>
+}
+
+struct Rasterizer {
+    image_cmds: HashMap<ImageKey, (ColorU, Option<TileSize>)>,
+}
+
+impl AsyncBlobImageRasterizer for Rasterizer {
+    fn rasterize(&mut self, requests: &[BlobImageParams]) -> Vec<(BlobImageRequest, BlobImageResult)> {
+        let requests: Vec<Command> = requests.into_iter().map(
+            |item| {
+                let (color, tile_size) = self.image_cmds[&item.request.key];
+
+                let tile = item.request.tile.map(|tile| (tile_size.unwrap(), tile));
+
+                Command {
+                    request: item.request,
+                    color,
+                    tile,
+                    descriptor: item.descriptor,
+                    dirty_rect: item.dirty_rect,
+                }
+            }
+        ).collect();
+
+        requests.iter().map(|cmd| {
+            (cmd.request, render_blob(cmd.color, &cmd.descriptor, cmd.tile, cmd.dirty_rect))
+        }).collect()
+    }
+}
--- a/gfx/wrench/src/egl.rs
+++ b/gfx/wrench/src/egl.rs
@@ -10,17 +10,17 @@ use glutin::CreationError;
 use glutin::GlAttributes;
 use glutin::GlContext;
 use glutin::GlRequest;
 use glutin::PixelFormat;
 use glutin::PixelFormatRequirements;
 use glutin::ReleaseBehavior;
 use glutin::Robustness;
 use glutin::Api;
-use glutin::dpi::PhysicalSize;
+use winit::dpi::PhysicalSize;
 
 use std::ffi::{CStr, CString};
 use std::os::raw::c_int;
 use std::{mem, ptr};
 use std::cell::Cell;
 
 use mozangle::egl::ffi as egl;
 mod ffi {
--- a/gfx/wrench/src/json_frame_writer.rs
+++ b/gfx/wrench/src/json_frame_writer.rs
@@ -172,16 +172,17 @@ impl JsonFrameWriter {
                         instance.key,
                         CachedFontInstance {
                             font_key: instance.font_key,
                             glyph_size: instance.glyph_size,
                         },
                     );
                 }
                 ResourceUpdate::DeleteFontInstance(_) => {}
+                ResourceUpdate::SetImageVisibleArea(..) => {}
             }
         }
     }
 
     fn next_rsrc_paths(
         prefix: &str,
         counter: &mut u32,
         base_path: &Path,
--- a/gfx/wrench/src/main.rs
+++ b/gfx/wrench/src/main.rs
@@ -58,33 +58,33 @@ mod yaml_frame_reader;
 mod yaml_frame_writer;
 mod yaml_helper;
 #[cfg(target_os = "macos")]
 mod cgfont_to_data;
 
 use binary_frame_reader::BinaryFrameReader;
 use gleam::gl;
 use glutin::GlContext;
-use glutin::dpi::{LogicalPosition, LogicalSize};
 use perf::PerfHarness;
 use png::save_flipped;
 use rawtest::RawtestHarness;
 use reftest::{ReftestHarness, ReftestOptions};
 #[cfg(feature = "headless")]
 use std::ffi::CString;
 #[cfg(feature = "headless")]
 use std::mem;
 use std::os::raw::c_void;
 use std::path::{Path, PathBuf};
 use std::process;
 use std::ptr;
 use std::rc::Rc;
 use std::sync::mpsc::{channel, Sender, Receiver};
 use webrender::DebugFlags;
 use webrender::api::*;
+use winit::dpi::{LogicalPosition, LogicalSize};
 use winit::VirtualKeyCode;
 use wrench::{Wrench, WrenchThing};
 use yaml_frame_reader::YamlFrameReader;
 
 lazy_static! {
     static ref PLATFORM_DEFAULT_FACE_NAME: String = String::from("Arial");
     static ref WHITE_COLOR: ColorF = ColorF::new(1.0, 1.0, 1.0, 1.0);
     static ref BLACK_COLOR: ColorF = ColorF::new(0.0, 0.0, 0.0, 1.0);
--- a/gfx/wrench/src/rawtest.rs
+++ b/gfx/wrench/src/rawtest.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 {WindowWrapper, NotifierEvent};
 use blob;
-use euclid::{TypedRect, TypedSize2D, TypedPoint2D};
+use euclid::{TypedRect, TypedSize2D, TypedPoint2D, point2, size2};
 use std::sync::Arc;
 use std::sync::atomic::{AtomicIsize, Ordering};
 use std::sync::mpsc::Receiver;
 use webrender::api::*;
 use wrench::Wrench;
 
 pub struct RawtestHarness<'a> {
     wrench: &'a mut Wrench,
@@ -42,16 +42,17 @@ impl<'a> RawtestHarness<'a> {
 
     pub fn run(mut self) {
         self.test_hit_testing();
         self.test_retained_blob_images_test();
         self.test_blob_update_test();
         self.test_blob_update_epoch_test();
         self.test_tile_decomposition();
         self.test_very_large_blob();
+        self.test_insufficient_blob_visible_area();
         self.test_offscreen_blob();
         self.test_save_restore();
         self.test_blur_cache();
         self.test_capture();
         self.test_zero_height_window();
     }
 
     fn render_and_get_pixels(&mut self, window_rect: DeviceUintRect) -> Vec<u8> {
@@ -175,26 +176,34 @@ impl<'a> RawtestHarness<'a> {
         builder.push_image(
             &info,
             image_size * 2.,
             image_size,
             ImageRendering::Auto,
             AlphaType::PremultipliedAlpha,
             blob_img,
         );
+        txn.set_image_visible_area(
+            blob_img,
+            NormalizedRect {
+                origin: point2(0.0, 0.03),
+                size: size2(1.0, 0.03),
+            }
+        );
+
         builder.pop_clip_id();
 
         let mut epoch = Epoch(0);
 
         self.submit_dl(&mut epoch, layout_size, builder, &txn.resource_updates);
 
         let pixels = self.render_and_get_pixels(window_rect);
 
         // make sure we didn't request too many blobs
-        assert_eq!(called.load(Ordering::SeqCst), 16);
+        assert!(called.load(Ordering::SeqCst) < 20);
 
         // make sure things are in the right spot
         assert!(
             pixels[(148 +
                 (window_rect.size.height as usize - 148) *
                     window_rect.size.width as usize) * 4] == 255 &&
                 pixels[(148 +
                     (window_rect.size.height as usize - 148) *
@@ -225,16 +234,109 @@ impl<'a> RawtestHarness<'a> {
         // confuses the `test_capture`. TODO: remove this
         txn = Transaction::new();
         txn.delete_image(blob_img);
         self.wrench.api.update_resources(txn.resource_updates);
 
         *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new();
     }
 
+    fn test_insufficient_blob_visible_area(&mut self) {
+        println!("\tinsufficient blob visible area.");
+
+        // This test compares two almost identical display lists containing the a blob
+        // image. The only difference is that one of the display lists specifies a visible
+        // area for its blob image which is too small, causing frame building to run into
+        // missing tiles, and forcing it to exercise the code path where missing tiles are
+        // rendered synchronously on demand.
+
+        assert_eq!(self.wrench.device_pixel_ratio, 1.);
+
+        let window_size = self.window.get_inner_size();
+        let test_size = DeviceUintSize::new(800, 800);
+        let window_rect = DeviceUintRect::new(
+            DeviceUintPoint::new(0, window_size.height - test_size.height),
+            test_size,
+        );
+        let layout_size = LayoutSize::new(800.0, 800.0);
+        let image_size = size(800.0, 800.0);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 0.0, 800.0, 800.0));
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let mut txn = Transaction::new();
+
+        let blob_img1 = self.wrench.api.generate_image_key();
+        txn.add_image(
+            blob_img1,
+            ImageDescriptor::new(
+                image_size.width as u32,
+                image_size.height as u32,
+                ImageFormat::BGRA8,
+                false,
+                false
+            ),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+            Some(100),
+        );
+
+        builder.push_image(
+            &info,
+            image_size,
+            image_size,
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img1,
+        );
+
+        self.submit_dl(&mut Epoch(0), layout_size, builder, &txn.resource_updates);
+        let pixels1 = self.render_and_get_pixels(window_rect);
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let mut txn = Transaction::new();
+
+        let blob_img2 = self.wrench.api.generate_image_key();
+        txn.add_image(
+            blob_img2,
+            ImageDescriptor::new(
+                image_size.width as u32,
+                image_size.height as u32,
+                ImageFormat::BGRA8,
+                false,
+                false
+            ),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+            Some(100),
+        );
+        // Set a visible rectangle that is too small.
+        // This will force sync rasterization of the missing tiles during frame building.
+        txn.set_image_visible_area(blob_img2, NormalizedRect {
+            origin: point2(0.25, 0.25),
+            size: size2(0.1, 0.1),
+        });
+
+        builder.push_image(
+            &info,
+            image_size,
+            image_size,
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img2,
+        );
+
+        self.submit_dl(&mut Epoch(1), layout_size, builder, &txn.resource_updates);
+        let pixels2 = self.render_and_get_pixels(window_rect);
+
+        assert!(pixels1 == pixels2);
+
+        txn = Transaction::new();
+        txn.delete_image(blob_img1);
+        txn.delete_image(blob_img2);
+        self.wrench.api.update_resources(txn.resource_updates);
+    }
+
     fn test_offscreen_blob(&mut self) {
         println!("\toffscreen blob update.");
 
         assert_eq!(self.wrench.device_pixel_ratio, 1.);
 
         let window_size = self.window.get_inner_size();
 
         let test_size = DeviceUintSize::new(800, 800);
@@ -461,22 +563,24 @@ impl<'a> RawtestHarness<'a> {
 
         // setup some counters to count how many times each image is requested
         let img1_requested = Arc::new(AtomicIsize::new(0));
         let img1_requested_inner = Arc::clone(&img1_requested);
         let img2_requested = Arc::new(AtomicIsize::new(0));
         let img2_requested_inner = Arc::clone(&img2_requested);
 
         // track the number of times that the second image has been requested
-        self.wrench.callbacks.lock().unwrap().request = Box::new(move |&desc| {
-            if desc.key == blob_img {
-                img1_requested_inner.fetch_add(1, Ordering::SeqCst);
-            }
-            if desc.key == blob_img2 {
-                img2_requested_inner.fetch_add(1, Ordering::SeqCst);
+        self.wrench.callbacks.lock().unwrap().request = Box::new(move |requests| {
+            for item in requests {
+                if item.request.key == blob_img {
+                    img1_requested_inner.fetch_add(1, Ordering::SeqCst);
+                }
+                if item.request.key == blob_img2 {
+                    img2_requested_inner.fetch_add(1, Ordering::SeqCst);
+                }
             }
         });
 
         // create two blob images and draw them
         let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
         let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
         let info2 = LayoutPrimitiveInfo::new(rect(200.0, 60.0, 200.0, 200.0));
         let push_images = |builder: &mut DisplayListBuilder| {
--- a/gfx/wrench/src/ron_frame_writer.rs
+++ b/gfx/wrench/src/ron_frame_writer.rs
@@ -136,16 +136,17 @@ impl RonFrameWriter {
                     }
                     &AddFont::Native(key, ref handle) => {
                         self.fonts.insert(key, CachedFont::Native(handle.clone()));
                     }
                 },
                 ResourceUpdate::DeleteFont(_) => {}
                 ResourceUpdate::AddFontInstance(_) => {}
                 ResourceUpdate::DeleteFontInstance(_) => {}
+                ResourceUpdate::SetImageVisibleArea(..) => {}
             }
         }
     }
 }
 
 impl fmt::Debug for RonFrameWriter {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "RonFrameWriter")
--- a/gfx/wrench/src/wrench.rs
+++ b/gfx/wrench/src/wrench.rs
@@ -209,17 +209,17 @@ impl Wrench {
             device_pixel_ratio: dp_ratio,
             resource_override_path: shader_override_path,
             recorder,
             enable_subpixel_aa: !no_subpixel_aa,
             debug_flags,
             enable_clear_scissor: !no_scissor,
             max_recorded_profiles: 16,
             precache_shaders,
-            blob_image_renderer: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))),
+            blob_image_handler: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))),
             disable_dual_source_blending,
             chase_primitive,
             ..Default::default()
         };
 
         // put an Awakened event into the queue to kick off the first frame
         if let Some(ref elp) = proxy {
             #[cfg(not(target_os = "android"))]
--- a/gfx/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wrench/src/yaml_frame_writer.rs
@@ -585,16 +585,17 @@ impl YamlFrameWriter {
                         instance.key,
                         CachedFontInstance {
                             font_key: instance.font_key,
                             glyph_size: instance.glyph_size,
                         },
                     );
                 }
                 ResourceUpdate::DeleteFontInstance(_) => {}
+                ResourceUpdate::SetImageVisibleArea(..) => {}
             }
         }
     }
 
 
 
     fn path_for_image(&mut self, key: ImageKey) -> Option<PathBuf> {
         let data = match self.images.get_mut(&key) {