Bug 1490640. Update webrender to 70edb5f8a75ea1e1440ba7984cc42df9eb05ae69
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Wed, 12 Sep 2018 17:20:35 -0400
changeset 436014 969f3b484bbbe66c285c0c8ad11f067000917e93
parent 436013 10b2b242b9bdf9daa261c66f269f622d51cb6a12
child 436015 f5063652dacb667a451348c37c983cf962eaf324
push id107778
push userjmuizelaar@mozilla.com
push dateThu, 13 Sep 2018 02:00:00 +0000
treeherdermozilla-inbound@f5063652dacb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1490640
milestone64.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 1490640. Update webrender to 70edb5f8a75ea1e1440ba7984cc42df9eb05ae69
gfx/webrender/src/clip.rs
gfx/webrender/src/clip_scroll_tree.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/resource_cache.rs
gfx/webrender/src/scene_builder.rs
gfx/webrender/src/spatial_node.rs
gfx/webrender/src/util.rs
gfx/webrender_api/src/api.rs
gfx/webrender_bindings/revision.txt
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -12,17 +12,17 @@ use clip_scroll_tree::{ClipScrollTree, C
 use ellipse::Ellipse;
 use gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use gpu_types::{BoxShadowStretchMode};
 use internal_types::FastHashSet;
 use prim_store::{ClipData, ImageMaskData, SpaceMapper};
 use render_task::to_cache_size;
 use resource_cache::{ImageRequest, ResourceCache};
 use std::{cmp, u32};
-use util::{extract_inner_rect_safe, pack_as_float, project_rect, recycle_vec};
+use util::{extract_inner_rect_safe, pack_as_float, project_rect, recycle_vec, ScaleOffset};
 
 /*
 
  Module Overview
 
  There are a number of data structures involved in the clip module:
 
  ClipStore - Main interface used by other modules.
@@ -195,17 +195,17 @@ pub struct ClipNodeRange {
 // A helper struct for converting between coordinate systems
 // of clip sources and primitives.
 // todo(gw): optimize:
 //  separate arrays for matrices
 //  cache and only build as needed.
 #[derive(Debug)]
 enum ClipSpaceConversion {
     Local,
-    Offset(LayoutVector2D),
+    ScaleOffset(ScaleOffset),
     Transform(LayoutToWorldTransform),
 }
 
 // Temporary information that is cached and reused
 // during building of a clip chain instance.
 struct ClipNodeInfo {
     conversion: ClipSpaceConversion,
     node_index: ClipNodeIndex,
@@ -533,19 +533,19 @@ impl ClipStore {
         for node_info in self.clip_node_info.drain(..) {
             let node = &mut self.clip_nodes[node_info.node_index.0 as usize];
 
             // See how this clip affects the prim region.
             let clip_result = match node_info.conversion {
                 ClipSpaceConversion::Local => {
                     node.item.get_clip_result(&local_bounding_rect)
                 }
-                ClipSpaceConversion::Offset(offset) => {
+                ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
                     has_non_local_clips = true;
-                    node.item.get_clip_result(&local_bounding_rect.translate(&-offset))
+                    node.item.get_clip_result(&scale_offset.unmap_rect(&local_bounding_rect))
                 }
                 ClipSpaceConversion::Transform(ref transform) => {
                     has_non_local_clips = true;
                     node.item.get_clip_result_complex(
                         transform,
                         &world_clip_rect,
                         world_rect,
                     )
@@ -571,17 +571,17 @@ impl ClipStore {
                     );
 
                     // Calculate some flags that are required for the segment
                     // building logic.
                     let flags = match node_info.conversion {
                         ClipSpaceConversion::Local => {
                             ClipNodeFlags::SAME_SPATIAL_NODE | ClipNodeFlags::SAME_COORD_SYSTEM
                         }
-                        ClipSpaceConversion::Offset(..) => {
+                        ClipSpaceConversion::ScaleOffset(..) => {
                             ClipNodeFlags::SAME_COORD_SYSTEM
                         }
                         ClipSpaceConversion::Transform(..) => {
                             ClipNodeFlags::empty()
                         }
                     };
 
                     // As a special case, a partial accept of a clip rect that is
@@ -1151,19 +1151,22 @@ fn add_clip_node_to_current_chain(
     let clip_spatial_node = &clip_scroll_tree.spatial_nodes[clip_node.spatial_node_index.0 as usize];
     let ref_spatial_node = &clip_scroll_tree.spatial_nodes[spatial_node_index.0];
 
     // Determine the most efficient way to convert between coordinate
     // systems of the primitive and clip node.
     let conversion = if spatial_node_index == clip_node.spatial_node_index {
         Some(ClipSpaceConversion::Local)
     } else if ref_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
-        let offset = clip_spatial_node.coordinate_system_relative_offset -
-                     ref_spatial_node.coordinate_system_relative_offset;
-        Some(ClipSpaceConversion::Offset(offset))
+        let scale_offset = ref_spatial_node
+            .coordinate_system_relative_scale_offset
+            .difference(
+                &clip_spatial_node.coordinate_system_relative_scale_offset
+            );
+        Some(ClipSpaceConversion::ScaleOffset(scale_offset))
     } else {
         let xf = clip_scroll_tree.get_relative_transform(
             clip_node.spatial_node_index,
             ROOT_SPATIAL_NODE_INDEX,
         );
 
         xf.map(|xf| {
             ClipSpaceConversion::Transform(xf.with_destination::<WorldPixel>())
@@ -1176,18 +1179,18 @@ fn add_clip_node_to_current_chain(
         if let Some(clip_rect) = clip_node.item.get_local_clip_rect() {
             match conversion {
                 ClipSpaceConversion::Local => {
                     *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
                         Some(rect) => rect,
                         None => return false,
                     };
                 }
-                ClipSpaceConversion::Offset(ref offset) => {
-                    let clip_rect = clip_rect.translate(offset);
+                ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
+                    let clip_rect = scale_offset.map_rect(&clip_rect);
                     *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
                         Some(rect) => rect,
                         None => return false,
                     };
                 }
                 ClipSpaceConversion::Transform(..) => {
                     // TODO(gw): In the future, we can reduce the size
                     //           of the pic_clip_rect here. To do this,
--- a/gfx/webrender/src/clip_scroll_tree.rs
+++ b/gfx/webrender/src/clip_scroll_tree.rs
@@ -1,47 +1,45 @@
 /* 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::{ExternalScrollId, LayoutPoint, LayoutRect, LayoutVector2D, LayoutVector3D};
+use api::{ExternalScrollId, LayoutPoint, LayoutRect, LayoutVector2D};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation};
 use api::{LayoutSize, LayoutTransform, PropertyBinding, ScrollSensitivity, WorldPoint};
 use gpu_types::TransformPalette;
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintTree, PrintTreePrinter};
 use scene::SceneProperties;
 use smallvec::SmallVec;
 use spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo};
-use util::LayoutToWorldFastTransform;
+use util::{LayoutToWorldFastTransform, ScaleOffset};
 
 pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
 
 /// An id that identifies coordinate systems in the ClipScrollTree. Each
 /// coordinate system has an id and those ids will be shared when the coordinates
 /// system are the same or are in the same axis-aligned space. This allows
 /// for optimizing mask generation.
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CoordinateSystemId(pub u32);
 
 /// A node in the hierarchy of coordinate system
 /// transforms.
 #[derive(Debug)]
 pub struct CoordinateSystem {
-    pub offset: LayoutVector3D,
     pub transform: LayoutTransform,
     pub parent: Option<CoordinateSystemId>,
 }
 
 impl CoordinateSystem {
     fn root() -> Self {
         CoordinateSystem {
-            offset: LayoutVector3D::zero(),
             transform: LayoutTransform::identity(),
             parent: None,
         }
     }
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, PartialOrd)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -82,18 +80,18 @@ pub struct TransformUpdateState {
     pub nearest_scrolling_ancestor_viewport: LayoutRect,
 
     /// An id for keeping track of the axis-aligned space of this node. This is used in
     /// order to to track what kinds of clip optimizations can be done for a particular
     /// display list item, since optimizations can usually only be done among
     /// coordinate systems which are relatively axis aligned.
     pub current_coordinate_system_id: CoordinateSystemId,
 
-    /// Offset from the coordinate system that started this compatible coordinate system.
-    pub coordinate_system_relative_offset: LayoutVector2D,
+    /// Scale and offset from the coordinate system that started this compatible coordinate system.
+    pub coordinate_system_relative_scale_offset: ScaleOffset,
 
     /// True if this node is transformed by an invertible transform.  If not, display items
     /// transformed by this node will not be displayed and display items not transformed by this
     /// node will not be clipped by clips that are transformed by this node.
     pub invertible: bool,
 }
 
 impl ClipScrollTree {
@@ -130,34 +128,27 @@ impl ClipScrollTree {
         while coordinate_system_id != parent.coordinate_system_id {
             nodes.push(coordinate_system_id);
             let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
             coordinate_system_id = coord_system.parent.expect("invalid parent!");
         }
 
         nodes.reverse();
 
-        let mut transform = LayoutTransform::create_translation(
-            -parent.coordinate_system_relative_offset.x,
-            -parent.coordinate_system_relative_offset.y,
-            0.0,
-        );
+        let mut transform = parent.coordinate_system_relative_scale_offset
+                                  .inverse()
+                                  .to_transform();
 
         for node in nodes {
             let coord_system = &self.coord_systems[node.0 as usize];
-            transform = transform.pre_translate(coord_system.offset)
-                                 .pre_mul(&coord_system.transform);
+            transform = transform.pre_mul(&coord_system.transform);
         }
 
-        let transform = transform.pre_translate(
-            LayoutVector3D::new(
-                child.coordinate_system_relative_offset.x,
-                child.coordinate_system_relative_offset.y,
-                0.0,
-            )
+        let transform = transform.pre_mul(
+            &child.coordinate_system_relative_scale_offset.to_transform(),
         );
 
         if inverse {
             transform.inverse()
         } else {
             Some(transform)
         }
     }
@@ -269,17 +260,17 @@ impl ClipScrollTree {
 
         let root_reference_frame_index = self.root_reference_frame_index();
         let mut state = TransformUpdateState {
             parent_reference_frame_transform: LayoutVector2D::new(pan.x, pan.y).into(),
             parent_accumulated_scroll_offset: LayoutVector2D::zero(),
             nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
             nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
             current_coordinate_system_id: CoordinateSystemId::root(),
-            coordinate_system_relative_offset: LayoutVector2D::zero(),
+            coordinate_system_relative_scale_offset: ScaleOffset::identity(),
             invertible: true,
         };
 
         self.update_node(
             root_reference_frame_index,
             &mut state,
             &mut transform_palette,
             scene_properties,
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -1070,16 +1070,32 @@ impl<'a> DisplayListFlattener<'a> {
                 true,
                 clip_chain_id,
                 spatial_node_index,
                 None,
                 PrimitiveContainer::Brush(container_prim),
             );
         }
 
+        // preserve-3d's semantics are to hoist all your children to be your siblings
+        // when doing backface-visibility checking, so we need to grab the backface-visibility
+        // of the lowest ancestor which *doesn't* preserve-3d, and AND it in with ours.
+        //
+        // No this isn't obvious or clear, it's just what we worked out over a day of testing.
+        // There's probably a bug in here, but I couldn't find it with the examples and tests
+        // at my disposal!
+        let ancestor_is_backface_visible =
+            self.sc_stack
+                .iter()
+                .rfind(|sc| sc.transform_style == TransformStyle::Flat)
+                .map(|sc| sc.is_backface_visible)
+                .unwrap_or(is_backface_visible);
+
+        let is_backface_visible = is_backface_visible && ancestor_is_backface_visible;
+
         // Push the SC onto the stack, so we know how to handle things in
         // pop_stacking_context.
         let sc = FlattenedStackingContext {
             is_backface_visible,
             pipeline_id,
             transform_style,
             establishes_3d_context,
             participating_in_3d_context,
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -7,17 +7,17 @@ use api::{DeviceIntRect, DeviceIntSize, 
 use api::{FilterOp, GlyphInstance, GradientStop, ImageKey, ImageRendering, ItemRange, ItemTag, TileOffset};
 use api::{GlyphRasterSpace, LayoutPoint, LayoutRect, LayoutSize, LayoutToWorldTransform, LayoutVector2D};
 use api::{PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat, DeviceIntSideOffsets, WorldPixel};
 use api::{BorderWidths, BoxShadowClipMode, LayoutToWorldScale, NormalBorder, WorldRect, PicturePixel, RasterPixel};
 use app_units::Au;
 use border::{BorderCacheKey, BorderRenderTaskInfo};
 use clip_scroll_tree::{ClipScrollTree, CoordinateSystemId, SpatialNodeIndex};
 use clip::{ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
-use euclid::{TypedVector2D, TypedTransform3D, TypedRect};
+use euclid::{TypedTransform3D, TypedRect};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveContext;
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::{GpuBlockData, GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest,
                 ToGpuBlocks};
 use gpu_types::BrushFlags;
 use image::{for_each_tile, for_each_repetition};
 use picture::{PictureCompositeMode, PicturePrimitive};
@@ -25,17 +25,17 @@ use picture::{PictureCompositeMode, Pict
 use render_backend::FrameId;
 use render_task::{BlitSource, RenderTask, RenderTaskCacheKey};
 use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
 use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
 use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
 use scene::SceneProperties;
 use segment::SegmentBuilder;
 use std::{cmp, fmt, mem, usize};
-use util::{MatrixHelpers, pack_as_float, recycle_vec, project_rect, raster_rect_to_device_pixels};
+use util::{ScaleOffset, MatrixHelpers, pack_as_float, recycle_vec, project_rect, raster_rect_to_device_pixels};
 
 
 const MIN_BRUSH_SPLIT_AREA: f32 = 256.0 * 256.0;
 pub const VECS_PER_SEGMENT: usize = 2;
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub struct ScrollNodeAndClipChain {
     pub spatial_node_index: SpatialNodeIndex,
@@ -90,17 +90,17 @@ impl PrimitiveOpacity {
             is_opaque: alpha == 1.0,
         }
     }
 }
 
 #[derive(Debug)]
 pub enum CoordinateSpaceMapping<F, T> {
     Local,
-    Offset(TypedVector2D<f32, F>),
+    ScaleOffset(ScaleOffset),
     Transform(TypedTransform3D<f32, F, T>),
 }
 
 #[derive(Debug)]
 pub struct SpaceMapper<F, T> {
     kind: CoordinateSpaceMapping<F, T>,
     pub ref_spatial_node_index: SpatialNodeIndex,
     pub current_target_spatial_node_index: SpatialNodeIndex,
@@ -140,23 +140,22 @@ impl<F, T> SpaceMapper<F, T> where F: fm
             let spatial_nodes = &clip_scroll_tree.spatial_nodes;
             let ref_spatial_node = &spatial_nodes[self.ref_spatial_node_index.0];
             let target_spatial_node = &spatial_nodes[target_node_index.0];
             self.current_target_spatial_node_index = target_node_index;
 
             self.kind = if self.ref_spatial_node_index == target_node_index {
                 CoordinateSpaceMapping::Local
             } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-                let offset = TypedVector2D::new(
-                    target_spatial_node.coordinate_system_relative_offset.x -
-                        ref_spatial_node.coordinate_system_relative_offset.x,
-                    target_spatial_node.coordinate_system_relative_offset.y -
-                        ref_spatial_node.coordinate_system_relative_offset.y,
-                );
-                CoordinateSpaceMapping::Offset(offset)
+                CoordinateSpaceMapping::ScaleOffset(
+                    ref_spatial_node.coordinate_system_relative_scale_offset
+                        .difference(
+                            &target_spatial_node.coordinate_system_relative_scale_offset
+                        )
+                )
             } else {
                 let transform = clip_scroll_tree.get_relative_transform(
                     target_node_index,
                     self.ref_spatial_node_index,
                 ).expect("bug: should have already been culled");
 
                 CoordinateSpaceMapping::Transform(
                     transform.with_source::<F>().with_destination::<T>()
@@ -165,47 +164,46 @@ impl<F, T> SpaceMapper<F, T> where F: fm
         }
     }
 
     pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 TypedTransform3D::identity()
             }
-            CoordinateSpaceMapping::Offset(offset) => {
-                TypedTransform3D::create_translation(offset.x, offset.y, 0.0)
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                scale_offset.to_transform()
             }
             CoordinateSpaceMapping::Transform(transform) => {
                 transform
             }
         }
     }
 
     pub fn unmap(&self, rect: &TypedRect<f32, T>) -> Option<TypedRect<f32, F>> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 Some(TypedRect::from_untyped(&rect.to_untyped()))
             }
-            CoordinateSpaceMapping::Offset(ref offset) => {
-                let offset = TypedVector2D::new(-offset.x, -offset.y);
-                Some(TypedRect::from_untyped(&rect.translate(&offset).to_untyped()))
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                Some(scale_offset.unmap_rect(rect))
             }
             CoordinateSpaceMapping::Transform(ref transform) => {
                 transform.inverse_rect_footprint(rect)
             }
         }
     }
 
     pub fn map(&self, rect: &TypedRect<f32, F>) -> Option<TypedRect<f32, T>> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 Some(TypedRect::from_untyped(&rect.to_untyped()))
             }
-            CoordinateSpaceMapping::Offset(ref offset) => {
-                Some(TypedRect::from_untyped(&rect.translate(offset).to_untyped()))
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                Some(scale_offset.map_rect(rect))
             }
             CoordinateSpaceMapping::Transform(ref transform) => {
                 match project_rect(transform, rect, &self.bounds) {
                     Some(bounds) => {
                         Some(bounds)
                     }
                     None => {
                         warn!("parent relative transform can't transform the primitive rect for {:?}", rect);
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -4,16 +4,17 @@
 
 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, ResourceUpdate, ImageKey};
+use api::{NotificationRequest, Checkpoint};
 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;
@@ -39,16 +40,17 @@ use serde_json;
 use std::path::PathBuf;
 use std::sync::atomic::{ATOMIC_USIZE_INIT, AtomicUsize, Ordering};
 use std::mem::replace;
 use std::sync::mpsc::{channel, Sender, Receiver};
 use std::u32;
 #[cfg(feature = "replay")]
 use tiling::Frame;
 use time::precise_time_ns;
+use util::drain_filter;
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Clone)]
 pub struct DocumentView {
     pub window_size: DeviceUintSize,
     pub inner_rect: DeviceUintRect,
     pub layer: DocumentLayer,
@@ -100,16 +102,21 @@ struct Document {
 
     /// A data structure to allow hit testing against rendered frames. This is updated
     /// every time we produce a fully rendered frame.
     hit_tester: Option<HitTester>,
 
     /// Properties that are resolved during frame building and can be changed at any time
     /// without requiring the scene to be re-built.
     dynamic_properties: SceneProperties,
+
+    /// Track whether the last built frame is up to date or if it will need to be re-built
+    /// before rendering again.
+    frame_is_valid: bool,
+    hit_tester_is_valid: bool,
 }
 
 impl Document {
     pub fn new(
         window_size: DeviceUintSize,
         layer: DocumentLayer,
         default_device_pixel_ratio: f32,
     ) -> Self {
@@ -126,16 +133,18 @@ impl Document {
                 device_pixel_ratio: default_device_pixel_ratio,
             },
             clip_scroll_tree: ClipScrollTree::new(),
             frame_id: FrameId(0),
             frame_builder: None,
             output_pipelines: FastHashSet::default(),
             hit_tester: None,
             dynamic_properties: SceneProperties::new(),
+            frame_is_valid: false,
+            hit_tester_is_valid: false,
         }
     }
 
     fn can_render(&self) -> bool {
         self.frame_builder.is_some() && self.scene.has_root_pipeline()
     }
 
     fn has_pixels(&self) -> bool {
@@ -168,17 +177,20 @@ impl Document {
                         hit_tester.find_node_under_point(test)
                     }
                     None => {
                         None
                     }
                 };
 
                 if self.hit_tester.is_some() {
-                    let _scrolled = self.scroll_nearest_scrolling_ancestor(delta, node_index);
+                    if self.scroll_nearest_scrolling_ancestor(delta, node_index) {
+                        self.hit_tester_is_valid = false;
+                        self.frame_is_valid = false;
+                    }
                 }
 
                 return DocumentOps {
                     // TODO: Does it make sense to track this as a scrolling even if we
                     // ended up not scrolling anything?
                     scroll: true,
                     ..DocumentOps::nop()
                 };
@@ -190,22 +202,29 @@ impl Document {
                         hit_tester.hit_test(HitTest::new(pipeline_id, point, flags))
                     }
                     None => HitTestResult { items: Vec::new() },
                 };
 
                 tx.send(result).unwrap();
             }
             FrameMsg::SetPan(pan) => {
-                self.view.pan = pan;
+                if self.view.pan != pan {
+                    self.view.pan = pan;
+                    self.hit_tester_is_valid = false;
+                    self.frame_is_valid = false;
+                }
             }
             FrameMsg::ScrollNodeWithId(origin, id, clamp) => {
                 profile_scope!("ScrollNodeWithScrollId");
 
-                let _scrolled = self.scroll_node(origin, id, clamp);
+                if self.scroll_node(origin, id, clamp) {
+                    self.hit_tester_is_valid = false;
+                    self.frame_is_valid = false;
+                }
 
                 return DocumentOps {
                     scroll: true,
                     ..DocumentOps::nop()
                 };
             }
             FrameMsg::GetScrollNodeState(tx) => {
                 profile_scope!("GetScrollNodeState");
@@ -246,16 +265,19 @@ impl Document {
                 &mut resource_profile.texture_cache,
                 &mut resource_profile.gpu_cache,
                 &self.dynamic_properties,
             );
             self.hit_tester = Some(frame_builder.create_hit_tester(&self.clip_scroll_tree));
             frame
         };
 
+        self.frame_is_valid = true;
+        self.hit_tester_is_valid = true;
+
         RenderedDocument {
             frame,
             is_new_scene,
         }
     }
 
     pub fn updated_pipeline_info(&mut self) -> PipelineInfo {
         let removed_pipelines = replace(&mut self.removed_pipelines, Vec::new());
@@ -290,40 +312,40 @@ impl Document {
     }
 
     pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
         self.clip_scroll_tree.get_scroll_node_state()
     }
 
     pub fn new_async_scene_ready(&mut self, built_scene: BuiltScene) {
         self.scene = built_scene.scene;
+        self.frame_is_valid = false;
+        self.hit_tester_is_valid = false;
 
         self.frame_builder = Some(built_scene.frame_builder);
 
         let old_scrolling_states = self.clip_scroll_tree.drain();
         self.clip_scroll_tree = built_scene.clip_scroll_tree;
         self.clip_scroll_tree.finalize_and_apply_pending_scroll_offsets(old_scrolling_states);
 
         // Advance to the next frame.
         self.frame_id.0 += 1;
     }
 }
 
 struct DocumentOps {
     scroll: bool,
     build_frame: bool,
-    render_frame: bool,
 }
 
 impl DocumentOps {
     fn nop() -> Self {
         DocumentOps {
             scroll: false,
             build_frame: false,
-            render_frame: false,
         }
     }
 }
 
 /// The unique id for WR resource identification.
 static NEXT_NAMESPACE_ID: AtomicUsize = ATOMIC_USIZE_INIT;
 
 #[cfg(any(feature = "capture", feature = "replay"))]
@@ -569,16 +591,17 @@ impl RenderBackend {
                         if let Some(rasterizer) = txn.blob_rasterizer.take() {
                             self.resource_cache.set_blob_rasterizer(rasterizer);
                         }
 
                         self.update_document(
                             txn.document_id,
                             replace(&mut txn.resource_updates, Vec::new()),
                             replace(&mut txn.frame_ops, Vec::new()),
+                            replace(&mut txn.notifications, Vec::new()),
                             txn.build_frame,
                             txn.render_frame,
                             &mut frame_counter,
                             &mut profile_counters,
                             has_built_scene,
                         );
                     },
                     SceneBuilderResult::FlushComplete(tx) => {
@@ -820,16 +843,17 @@ impl RenderBackend {
             removed_pipelines: Vec::new(),
             epoch_updates: Vec::new(),
             request_scene_build: None,
             blob_rasterizer: None,
             blob_requests: Vec::new(),
             resource_updates: transaction_msg.resource_updates,
             frame_ops: transaction_msg.frame_ops,
             rasterized_blobs: Vec::new(),
+            notifications: transaction_msg.notifications,
             set_root_pipeline: None,
             build_frame: transaction_msg.generate_frame,
             render_frame: transaction_msg.generate_frame,
         });
 
         self.resource_cache.pre_scene_building_update(
             &mut txn.resource_updates,
             &mut profile_counters.resources,
@@ -855,16 +879,17 @@ impl RenderBackend {
             txn.blob_rasterizer = blob_rasterizer;
         }
 
         if !transaction_msg.use_scene_builder_thread && txn.can_skip_scene_builder() {
             self.update_document(
                 txn.document_id,
                 replace(&mut txn.resource_updates, Vec::new()),
                 replace(&mut txn.frame_ops, Vec::new()),
+                replace(&mut txn.notifications, Vec::new()),
                 txn.build_frame,
                 txn.render_frame,
                 frame_counter,
                 profile_counters,
                 false
             );
 
             return;
@@ -891,73 +916,84 @@ impl RenderBackend {
         tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
     }
 
     fn update_document(
         &mut self,
         document_id: DocumentId,
         resource_updates: Vec<ResourceUpdate>,
         mut frame_ops: Vec<FrameMsg>,
+        mut notifications: Vec<NotificationRequest>,
         mut build_frame: bool,
         mut render_frame: bool,
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
         has_built_scene: bool,
     ) {
         let requested_frame = render_frame;
-        self.resource_cache.post_scene_building_update(
-            resource_updates,
-            &mut profile_counters.resources,
-        );
 
         // If we have a sampler, get more frame ops from it and add them
         // to the transaction. This is a hook to allow the WR user code to
         // fiddle with things after a potentially long scene build, but just
         // before rendering. This is useful for rendering with the latest
         // async transforms.
         if build_frame {
             if let Some(ref sampler) = self.sampler {
                 frame_ops.append(&mut sampler.sample());
             }
         }
 
-
         let doc = self.documents.get_mut(&document_id).unwrap();
 
+        // TODO: this scroll variable doesn't necessarily mean we scrolled. It is only used
+        // for something wrench specific and we should remove it.
         let mut scroll = false;
         for frame_msg in frame_ops {
             let _timer = profile_counters.total_time.timer();
             let op = doc.process_frame_msg(frame_msg);
             build_frame |= op.build_frame;
-            render_frame |= op.render_frame;
             scroll |= op.scroll;
         }
 
+        for update in &resource_updates {
+            if let ResourceUpdate::UpdateImage(..) = update {
+                doc.frame_is_valid = false;
+            }
+        }
+
+        self.resource_cache.post_scene_building_update(
+            resource_updates,
+            &mut profile_counters.resources,
+        );
+
         // After applying the new scene we need to
         // rebuild the hit-tester, so we trigger a frame generation
         // step.
         //
         // TODO: We could avoid some the cost of building the frame by only
         // building the information required for hit-testing (See #2807).
         build_frame |= has_built_scene;
 
         if doc.dynamic_properties.flush_pending_updates() {
+            doc.frame_is_valid = false;
+            doc.hit_tester_is_valid = false;
             build_frame = true;
         }
 
         if !doc.can_render() {
             // TODO: this happens if we are building the first scene asynchronously and
             // scroll at the same time. we should keep track of the fact that we skipped
             // composition here and do it as soon as we receive the scene.
             build_frame = false;
             render_frame = false;
         }
 
-        // If we don't generate a frame it makes no sense to render.
-        debug_assert!(build_frame || !render_frame);
+        if doc.frame_is_valid {
+            build_frame = false;
+        }
 
         let mut frame_build_time = None;
         if build_frame && doc.has_pixels() {
             profile_scope!("generate frame");
 
             *frame_counter += 1;
 
             // borrow ck hack for profile_counters
@@ -991,25 +1027,31 @@ impl RenderBackend {
             let msg = ResultMsg::PublishDocument(
                 document_id,
                 rendered_document,
                 pending_update,
                 profile_counters.clone()
             );
             self.result_tx.send(msg).unwrap();
             profile_counters.reset();
-        } else if build_frame {
+        } else if requested_frame {
             // WR-internal optimization to avoid doing a bunch of render work if
             // there's no pixels. We still want to pretend to render and request
             // a render to make sure that the callbacks (particularly the
             // new_frame_ready callback below) has the right flags.
             let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
             self.result_tx.send(msg).unwrap();
         }
 
+        drain_filter(
+            &mut notifications,
+            |n| { n.when() == Checkpoint::FrameBuilt },
+            |n| { n.notify(); },
+        );
+
         // Always forward the transaction to the renderer if a frame was requested,
         // otherwise gecko can get into a state where it waits (forever) for the
         // transaction to complete before sending new work.
         if requested_frame {
             self.notifier.new_frame_ready(document_id, scroll, render_frame, frame_build_time);
         }
     }
 
@@ -1282,16 +1324,18 @@ impl RenderBackend {
                 removed_pipelines: Vec::new(),
                 view: view.clone(),
                 clip_scroll_tree: ClipScrollTree::new(),
                 frame_id: FrameId(0),
                 frame_builder: Some(FrameBuilder::empty()),
                 output_pipelines: FastHashSet::default(),
                 dynamic_properties: SceneProperties::new(),
                 hit_tester: None,
+                frame_is_valid: false,
+                hit_tester_is_valid: false,
             };
 
             let frame_name = format!("frame-{}-{}", (id.0).0, id.1);
             let frame = CaptureConfig::deserialize::<Frame, _>(root, frame_name);
             let build_frame = match frame {
                 Some(frame) => {
                     info!("\tloaded a built frame with {} passes", frame.passes.len());
 
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -37,16 +37,17 @@ use std::collections::hash_map::ValuesMu
 use std::{cmp, mem};
 use std::fmt::Debug;
 use std::hash::Hash;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use std::path::PathBuf;
 use std::sync::{Arc, RwLock};
 use texture_cache::{TextureCache, TextureCacheHandle, Eviction};
 use tiling::SpecialRenderPasses;
+use util::drain_filter;
 
 const DEFAULT_TILE_SIZE: TileSize = 512;
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct GlyphFetchResult {
     pub index_in_text_run: i32,
     pub uv_rect_address: GpuCacheAddress,
@@ -513,19 +514,18 @@ impl ResourceCache {
         }
     }
 
     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 {
+        for update in updates.iter() {
+            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),
                         );
@@ -536,59 +536,64 @@ impl ResourceCache {
                         self.update_blob_image(
                             img.key,
                             &img.descriptor,
                             &img.dirty_rect,
                             Arc::clone(blob_data)
                         );
                     }
                 }
-                ResourceUpdate::SetImageVisibleArea(key, area) => {
+                ResourceUpdate::SetImageVisibleArea(ref key, ref 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 {
+        drain_filter(
+            updates,
+            |update| match *update {
+                ResourceUpdate::AddFont(_) |
+                ResourceUpdate::AddFontInstance(_) => true,
+                _ => false,
+            },
+            // Updates that were moved out of the array:
+            |update: ResourceUpdate| 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) => {
+                ResourceUpdate::AddFontInstance(instance) => {
                     self.add_font_instance(
                         instance.key,
                         instance.font_key,
                         instance.glyph_size,
                         instance.options,
                         instance.platform_options,
                         instance.variations,
                     );
                 }
-                other => {
-                    new_updates.push(other);
-                }
+                _ => { unreachable!(); }
             }
-        }
-
-        *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 {
--- a/gfx/webrender/src/scene_builder.rs
+++ b/gfx/webrender/src/scene_builder.rs
@@ -1,40 +1,42 @@
 /* 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, Epoch};
-use api::{BuiltDisplayList, ColorF, LayoutSize};
+use api::{BuiltDisplayList, ColorF, LayoutSize, NotificationRequest, Checkpoint};
 use api::channel::MsgSender;
 use frame_builder::{FrameBuilderConfig, FrameBuilder};
 use clip_scroll_tree::ClipScrollTree;
 use display_list_flattener::DisplayListFlattener;
 use internal_types::{FastHashMap, FastHashSet};
 use resource_cache::FontInstanceMap;
 use render_backend::DocumentView;
 use renderer::{PipelineInfo, SceneBuilderHooks};
 use scene::Scene;
 use std::sync::mpsc::{channel, Receiver, Sender};
 use std::mem::replace;
 use time::precise_time_ns;
+use util::drain_filter;
 
 /// Represents the work associated to a transaction before scene building.
 pub struct Transaction {
     pub document_id: DocumentId,
     pub display_list_updates: Vec<DisplayListUpdate>,
     pub removed_pipelines: Vec<PipelineId>,
     pub epoch_updates: Vec<(PipelineId, Epoch)>,
     pub request_scene_build: Option<SceneRequest>,
     pub blob_requests: Vec<BlobImageParams>,
     pub blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
     pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub frame_ops: Vec<FrameMsg>,
+    pub notifications: Vec<NotificationRequest>,
     pub set_root_pipeline: Option<PipelineId>,
     pub build_frame: bool,
     pub render_frame: bool,
 }
 
 impl Transaction {
     pub fn can_skip_scene_builder(&self) -> bool {
         self.request_scene_build.is_none() &&
@@ -56,16 +58,17 @@ impl Transaction {
 pub struct BuiltTransaction {
     pub document_id: DocumentId,
     pub built_scene: Option<BuiltScene>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
     pub blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
     pub frame_ops: Vec<FrameMsg>,
     pub removed_pipelines: Vec<PipelineId>,
+    pub notifications: Vec<NotificationRequest>,
     pub scene_build_start_time: u64,
     pub scene_build_end_time: u64,
     pub build_frame: bool,
     pub render_frame: bool,
 }
 
 pub struct DisplayListUpdate {
     pub pipeline_id: PipelineId,
@@ -246,16 +249,17 @@ impl SceneBuilder {
                 build_frame: true,
                 render_frame: item.build_frame,
                 built_scene,
                 resource_updates: Vec::new(),
                 rasterized_blobs: Vec::new(),
                 blob_rasterizer: None,
                 frame_ops: Vec::new(),
                 removed_pipelines: Vec::new(),
+                notifications: Vec::new(),
                 scene_build_start_time,
                 scene_build_end_time: precise_time_ns(),
             });
 
             self.forward_built_transaction(txn);
         }
     }
 
@@ -317,26 +321,33 @@ impl SceneBuilder {
 
         let blob_requests = replace(&mut txn.blob_requests, Vec::new());
         let mut rasterized_blobs = txn.blob_rasterizer.as_mut().map_or(
             Vec::new(),
             |rasterizer| rasterizer.rasterize(&blob_requests),
         );
         rasterized_blobs.append(&mut txn.rasterized_blobs);
 
+        drain_filter(
+            &mut txn.notifications,
+            |n| { n.when() == Checkpoint::SceneBuilt },
+            |n| { n.notify(); },
+        );
+
         Box::new(BuiltTransaction {
             document_id: txn.document_id,
             build_frame: txn.build_frame || built_scene.is_some(),
             render_frame: txn.render_frame,
             built_scene,
             rasterized_blobs,
             resource_updates: replace(&mut txn.resource_updates, Vec::new()),
             blob_rasterizer: replace(&mut txn.blob_rasterizer, None),
             frame_ops: replace(&mut txn.frame_ops, Vec::new()),
             removed_pipelines: replace(&mut txn.removed_pipelines, Vec::new()),
+            notifications: replace(&mut txn.notifications, Vec::new()),
             scene_build_start_time,
             scene_build_end_time: precise_time_ns(),
         })
     }
 
     /// Send the result of process_transaction back to the render backend.
     fn forward_built_transaction(&mut self, txn: Box<BuiltTransaction>) {
         // We only need the pipeline info and the result channel if we
--- a/gfx/webrender/src/spatial_node.rs
+++ b/gfx/webrender/src/spatial_node.rs
@@ -1,21 +1,21 @@
 
 /* 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::{ExternalScrollId, LayoutPixel, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform};
 use api::{LayoutVector2D, PipelineId, PropertyBinding, ScrollClamping, ScrollLocation};
-use api::{ScrollSensitivity, StickyOffsetBounds, LayoutVector3D};
+use api::{ScrollSensitivity, StickyOffsetBounds};
 use clip_scroll_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
 use euclid::SideOffsets2D;
 use gpu_types::TransformPalette;
 use scene::SceneProperties;
-use util::{LayoutFastTransform, LayoutToWorldFastTransform, MatrixHelpers, TransformedRectKind};
+use util::{LayoutFastTransform, LayoutToWorldFastTransform, ScaleOffset, TransformedRectKind};
 
 #[derive(Clone, Debug)]
 pub enum SpatialNodeType {
     /// A special kind of node that adjusts its position based on the position
     /// of its parent node and a given set of sticky positioning offset bounds.
     /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
     /// https://www.w3.org/TR/css-position-3/#sticky-pos
     StickyFrame(StickyFrameInfo),
@@ -61,17 +61,17 @@ pub struct SpatialNode {
     pub invertible: bool,
 
     /// The axis-aligned coordinate system id of this node.
     pub coordinate_system_id: CoordinateSystemId,
 
     /// The transformation from the coordinate system which established our compatible coordinate
     /// system (same coordinate system id) and us. This can change via scroll offsets and via new
     /// reference frame transforms.
-    pub coordinate_system_relative_offset: LayoutVector2D,
+    pub coordinate_system_relative_scale_offset: ScaleOffset,
 }
 
 impl SpatialNode {
     pub fn new(
         pipeline_id: PipelineId,
         parent_index: Option<SpatialNodeIndex>,
         node_type: SpatialNodeType,
     ) -> Self {
@@ -80,17 +80,17 @@ impl SpatialNode {
             world_content_transform: LayoutToWorldFastTransform::identity(),
             transform_kind: TransformedRectKind::AxisAligned,
             parent: parent_index,
             children: Vec::new(),
             pipeline_id,
             node_type,
             invertible: true,
             coordinate_system_id: CoordinateSystemId(0),
-            coordinate_system_relative_offset: LayoutVector2D::zero(),
+            coordinate_system_relative_scale_offset: ScaleOffset::identity(),
         }
     }
 
     pub fn new_scroll_frame(
         pipeline_id: PipelineId,
         parent_index: SpatialNodeIndex,
         external_id: Option<ExternalScrollId>,
         frame_rect: &LayoutRect,
@@ -270,37 +270,38 @@ impl SpatialNode {
 
                 info.invertible = self.world_viewport_transform.is_invertible();
                 if !info.invertible {
                     return;
                 }
 
                 // Try to update our compatible coordinate system transform. If we cannot, start a new
                 // incompatible coordinate system.
-                if relative_transform.is_simple_2d_translation() {
-                    self.coordinate_system_relative_offset =
-                        state.coordinate_system_relative_offset +
-                        LayoutVector2D::new(relative_transform.m41, relative_transform.m42);
-                } else {
-                    // If we break 2D axis alignment or have a perspective component, we need to start a
-                    // new incompatible coordinate system with which we cannot share clips without masking.
-                    self.coordinate_system_relative_offset = LayoutVector2D::zero();
+                match ScaleOffset::from_transform(&relative_transform) {
+                    Some(ref scale_offset) => {
+                        self.coordinate_system_relative_scale_offset =
+                            state.coordinate_system_relative_scale_offset.accumulate(scale_offset);
+                    }
+                    None => {
+                        // If we break 2D axis alignment or have a perspective component, we need to start a
+                        // new incompatible coordinate system with which we cannot share clips without masking.
+                        self.coordinate_system_relative_scale_offset = ScaleOffset::identity();
 
-                    // Push that new coordinate system and record the new id.
-                    let coord_system = CoordinateSystem {
-                        offset: LayoutVector3D::new(
-                            state.coordinate_system_relative_offset.x,
-                            state.coordinate_system_relative_offset.y,
-                            0.0,
-                        ),
-                        transform: relative_transform,
-                        parent: Some(state.current_coordinate_system_id),
-                    };
-                    state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
-                    coord_systems.push(coord_system);
+                        let transform = state.coordinate_system_relative_scale_offset
+                                             .to_transform()
+                                             .pre_mul(&relative_transform);
+
+                        // Push that new coordinate system and record the new id.
+                        let coord_system = CoordinateSystem {
+                            transform,
+                            parent: Some(state.current_coordinate_system_id),
+                        };
+                        state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
+                        coord_systems.push(coord_system);
+                    }
                 }
 
                 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,
@@ -322,18 +323,18 @@ impl SpatialNode {
                 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
                 };
 
                 let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
-                self.coordinate_system_relative_offset =
-                    state.coordinate_system_relative_offset + added_offset;
+                self.coordinate_system_relative_scale_offset =
+                    state.coordinate_system_relative_scale_offset.offset(added_offset.to_untyped());
 
                 if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
                     info.current_offset = sticky_offset;
                 }
 
                 self.coordinate_system_id = state.current_coordinate_system_id;
             }
         }
@@ -470,17 +471,17 @@ impl SpatialNode {
                 state.parent_accumulated_scroll_offset =
                     scrolling.offset + state.parent_accumulated_scroll_offset;
                 state.nearest_scrolling_ancestor_offset = scrolling.offset;
                 state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
             }
             SpatialNodeType::ReferenceFrame(ref info) => {
                 state.parent_reference_frame_transform = self.world_viewport_transform;
                 state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
-                state.coordinate_system_relative_offset = self.coordinate_system_relative_offset;
+                state.coordinate_system_relative_scale_offset = self.coordinate_system_relative_scale_offset;
                 let translation = -info.origin_in_parent_reference_frame;
                 state.nearest_scrolling_ancestor_viewport =
                     state.nearest_scrolling_ancestor_viewport
                        .translate(&translation);
             }
         }
     }
 
--- a/gfx/webrender/src/util.rs
+++ b/gfx/webrender/src/util.rs
@@ -1,25 +1,180 @@
 /* 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::{LayoutPixel, DeviceRect, WorldPixel, RasterRect};
-use euclid::{Point2D, Rect, Size2D, TypedPoint2D, TypedRect, TypedSize2D};
+use euclid::{Point2D, Rect, Size2D, TypedPoint2D, TypedRect, TypedSize2D, Vector2D};
 use euclid::{TypedTransform2D, TypedTransform3D, TypedVector2D, TypedScale};
 use num_traits::Zero;
 use plane_split::{Clipper, Polygon};
 use std::{i32, f32, fmt};
 use std::borrow::Cow;
 
-
 // Matches the definition of SK_ScalarNearlyZero in Skia.
 const NEARLY_ZERO: f32 = 1.0 / 4096.0;
 
+// Represents an optimized transform where there is only
+// a scale and translation (which are guaranteed to maintain
+// an axis align rectangle under transformation). The
+// scaling is applied first, followed by the translation.
+// TODO(gw): We should try and incorporate F <-> T units here,
+//           but it's a bit tricky to do that now with the
+//           way the current clip-scroll tree works.
+#[derive(Debug, Clone, Copy)]
+pub struct ScaleOffset {
+    pub scale: Vector2D<f32>,
+    pub offset: Vector2D<f32>,
+}
+
+impl ScaleOffset {
+    pub fn identity() -> Self {
+        ScaleOffset {
+            scale: Vector2D::new(1.0, 1.0),
+            offset: Vector2D::zero(),
+        }
+    }
+
+    // Construct a ScaleOffset from a transform. Returns
+    // None if the matrix is not a pure scale / translation.
+    pub fn from_transform<F, T>(
+        m: &TypedTransform3D<f32, F, T>,
+    ) -> Option<ScaleOffset> {
+
+        // To check that we have a pure scale / translation:
+        // Every field must match an identity matrix, except:
+        //  - Any value present in tx,ty
+        //  - Any non-neg value present in sx,sy (avoid negative for reflection/rotation)
+
+        if m.m11 < 0.0 ||
+           m.m12.abs() > NEARLY_ZERO ||
+           m.m13.abs() > NEARLY_ZERO ||
+           m.m14.abs() > NEARLY_ZERO ||
+           m.m21.abs() > NEARLY_ZERO ||
+           m.m22 < 0.0 ||
+           m.m23.abs() > NEARLY_ZERO ||
+           m.m24.abs() > NEARLY_ZERO ||
+           m.m31.abs() > NEARLY_ZERO ||
+           m.m32.abs() > NEARLY_ZERO ||
+           (m.m33 - 1.0).abs() > NEARLY_ZERO ||
+           m.m34.abs() > NEARLY_ZERO ||
+           m.m43.abs() > NEARLY_ZERO ||
+           (m.m44 - 1.0).abs() > NEARLY_ZERO {
+            return None;
+        }
+
+        Some(ScaleOffset {
+            scale: Vector2D::new(m.m11, m.m22),
+            offset: Vector2D::new(m.m41, m.m42),
+        })
+    }
+
+    pub fn inverse(&self) -> Self {
+        ScaleOffset {
+            scale: Vector2D::new(
+                1.0 / self.scale.x,
+                1.0 / self.scale.y,
+            ),
+            offset: Vector2D::new(
+                -self.offset.x / self.scale.x,
+                -self.offset.y / self.scale.y,
+            ),
+        }
+    }
+
+    pub fn offset(&self, offset: Vector2D<f32>) -> Self {
+        ScaleOffset {
+            scale: self.scale,
+            offset: self.offset + offset,
+        }
+    }
+
+    // Produce a ScaleOffset that includes both self
+    // and other. The 'self' ScaleOffset is applied
+    // after other.
+    pub fn accumulate(&self, other: &ScaleOffset) -> Self {
+        ScaleOffset {
+            scale: Vector2D::new(
+                self.scale.x * other.scale.x,
+                self.scale.y * other.scale.y,
+            ),
+            offset: Vector2D::new(
+                self.offset.x + self.scale.x * other.offset.x,
+                self.offset.y + self.scale.y * other.offset.y,
+            ),
+        }
+    }
+
+    // Find the difference between two ScaleOffset types.
+    pub fn difference(&self, other: &ScaleOffset) -> Self {
+        ScaleOffset {
+            scale: Vector2D::new(
+                other.scale.x / self.scale.x,
+                other.scale.y / self.scale.y,
+            ),
+            offset: Vector2D::new(
+                (other.offset.x - self.offset.x) / self.scale.x,
+                (other.offset.y - self.offset.y) / self.scale.y,
+            ),
+        }
+    }
+
+    pub fn map_rect<F, T>(&self, rect: &TypedRect<f32, F>) -> TypedRect<f32, T> {
+        TypedRect::new(
+            TypedPoint2D::new(
+                rect.origin.x * self.scale.x + self.offset.x,
+                rect.origin.y * self.scale.y + self.offset.y,
+            ),
+            TypedSize2D::new(
+                rect.size.width * self.scale.x,
+                rect.size.height * self.scale.y,
+            )
+        )
+    }
+
+    pub fn unmap_rect<F, T>(&self, rect: &TypedRect<f32, F>) -> TypedRect<f32, T> {
+        TypedRect::new(
+            TypedPoint2D::new(
+                (rect.origin.x - self.offset.x) / self.scale.x,
+                (rect.origin.y - self.offset.y) / self.scale.y,
+            ),
+            TypedSize2D::new(
+                rect.size.width / self.scale.x,
+                rect.size.height / self.scale.y,
+            )
+        )
+    }
+
+    pub fn to_transform<F, T>(&self) -> TypedTransform3D<f32, F, T> {
+        TypedTransform3D::row_major(
+            self.scale.x,
+            0.0,
+            0.0,
+            0.0,
+
+            0.0,
+            self.scale.y,
+            0.0,
+            0.0,
+
+            0.0,
+            0.0,
+            1.0,
+            0.0,
+
+            self.offset.x,
+            self.offset.y,
+            0.0,
+            1.0,
+        )
+    }
+}
+
 // TODO: Implement these in euclid!
 pub trait MatrixHelpers<Src, Dst> {
     fn preserves_2d_axis_alignment(&self) -> bool;
     fn has_perspective_component(&self) -> bool;
     fn has_2d_inverse(&self) -> bool;
     fn exceeds_2d_scale(&self, limit: f64) -> bool;
     fn inverse_project(&self, target: &TypedPoint2D<f32, Dst>) -> Option<TypedPoint2D<f32, Src>>;
     fn inverse_rect_footprint(&self, rect: &TypedRect<f32, Dst>) -> Option<TypedRect<f32, Src>>;
@@ -507,8 +662,68 @@ pub fn project_rect<F, T>(
 pub fn raster_rect_to_device_pixels(
     rect: RasterRect,
     device_pixel_scale: DevicePixelScale,
 ) -> DeviceRect {
     let world_rect = rect * TypedScale::new(1.0);
     let device_rect = world_rect * device_pixel_scale;
     device_rect.round_out()
 }
+
+/// Run the first callback over all elements in the array. If the callback returns true,
+/// the element is removed from the array and moved to a second callback.
+///
+/// This is a simple implementation waiting for Vec::drain_filter to be stable.
+/// When that happens, code like:
+///
+/// let filter = |op| {
+///     match *op {
+///         Enum::Foo | Enum::Bar => true,
+///         Enum::Baz => false,
+///     }
+/// };
+/// drain_filter(
+///     &mut ops,
+///     filter,
+///     |op| {
+///         match op {
+///             Enum::Foo => { foo(); }
+///             Enum::Bar => { bar(); }
+///             Enum::Baz => { unreachable!(); }
+///         }
+///     },
+/// );
+///
+/// Can be rewritten as:
+///
+/// let filter = |op| {
+///     match *op {
+///         Enum::Foo | Enum::Bar => true,
+///         Enum::Baz => false,
+///     }
+/// };
+/// for op in ops.drain_filter(filter) {
+///     match op {
+///         Enum::Foo => { foo(); }
+///         Enum::Bar => { bar(); }
+///         Enum::Baz => { unreachable!(); }
+///     }
+/// }
+///
+/// See https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter
+pub fn drain_filter<T, Filter, Action>(
+    vec: &mut Vec<T>,
+    mut filter: Filter,
+    mut action: Action,
+)
+where
+    Filter: FnMut(&mut T) -> bool,
+    Action: FnMut(T)
+{
+    let mut i = 0;
+    while i != vec.len() {
+        if filter(&mut vec[i]) {
+            action(vec.remove(i));
+        } else {
+            i += 1;
+        }
+    }
+}
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -5,16 +5,17 @@
 extern crate serde_bytes;
 
 use app_units::Au;
 use channel::{self, MsgSender, Payload, PayloadSender, PayloadSenderHelperMethods};
 use std::cell::Cell;
 use std::fmt;
 use std::marker::PhantomData;
 use std::path::PathBuf;
+use std::sync::Arc;
 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, NormalizedRect};
 
 pub type TileSize = u16;
@@ -43,16 +44,18 @@ pub struct Transaction {
     // Operations affecting the scene (applied before scene building).
     scene_ops: Vec<SceneMsg>,
     // Operations affecting the generation of frames (applied after scene building).
     frame_ops: Vec<FrameMsg>,
 
     // Additional display list data.
     payloads: Vec<Payload>,
 
+    notifications: Vec<NotificationRequest>,
+
     // Resource updates are applied after scene building.
     pub resource_updates: Vec<ResourceUpdate>,
 
     // If true the transaction is piped through the scene building thread, if false
     // it will be applied directly on the render backend.
     use_scene_builder_thread: bool,
 
     generate_frame: bool,
@@ -62,16 +65,17 @@ pub struct Transaction {
 
 impl Transaction {
     pub fn new() -> Self {
         Transaction {
             scene_ops: Vec::new(),
             frame_ops: Vec::new(),
             resource_updates: Vec::new(),
             payloads: Vec::new(),
+            notifications: Vec::new(),
             use_scene_builder_thread: true,
             generate_frame: false,
             low_priority: false,
         }
     }
 
     // TODO: better name?
     pub fn skip_scene_builder(&mut self) {
@@ -83,17 +87,18 @@ impl Transaction {
     pub fn use_scene_builder_thread(&mut self) {
         self.use_scene_builder_thread = true;
     }
 
     pub fn is_empty(&self) -> bool {
         !self.generate_frame &&
             self.scene_ops.is_empty() &&
             self.frame_ops.is_empty() &&
-            self.resource_updates.is_empty()
+            self.resource_updates.is_empty() &&
+            self.notifications.is_empty()
     }
 
     pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
         // We track epochs before and after scene building.
         // This one will be applied to the pending scene right away:
         self.scene_ops.push(SceneMsg::UpdateEpoch(pipeline_id, epoch));
         // And this one will be applied to the currently built scene at the end
         // of the transaction (potentially long after the scene_ops one).
@@ -166,16 +171,31 @@ impl Transaction {
         );
         self.payloads.push(Payload { epoch, pipeline_id, display_list_data });
     }
 
     pub fn update_resources(&mut self, resources: Vec<ResourceUpdate>) {
         self.merge(resources);
     }
 
+    // Note: Gecko uses this to get notified when a transaction that contains
+    // potentially long blob rasterization or scene build is ready to be rendered.
+    // so that the tab-switching integration can react adequately when tab
+    // switching takes too long. For this use case when matters is that the
+    // notification doesn't fire before scene building and blob rasterization.
+
+    /// Trigger a notification at a certain stage of the rendering pipeline.
+    ///
+    /// Not that notification requests are skipped during serialization, so is is
+    /// best to use them for synchronization purposes and not for things that could
+    /// affect the WebRender's state.
+    pub fn notify(&mut self, event: NotificationRequest) {
+        self.notifications.push(event);
+    }
+
     pub fn set_window_parameters(
         &mut self,
         window_size: DeviceUintSize,
         inner_rect: DeviceUintRect,
         device_pixel_ratio: f32,
     ) {
         self.scene_ops.push(
             SceneMsg::SetWindowParameters {
@@ -214,17 +234,17 @@ impl Transaction {
     pub fn set_pan(&mut self, pan: DeviceIntPoint) {
         self.frame_ops.push(FrameMsg::SetPan(pan));
     }
 
     /// Generate a new frame. When it's done and a RenderNotifier has been set
     /// in `webrender::Renderer`, [new_frame_ready()][notifier] gets called.
     /// Note that the notifier is called even if the frame generation was a
     /// no-op; the arguments passed to `new_frame_ready` will provide information
-    /// as to what happened.
+    /// as to when happened.
     ///
     /// [notifier]: trait.RenderNotifier.html#tymethod.new_frame_ready
     pub fn generate_frame(&mut self) {
         self.generate_frame = true;
     }
 
     /// Supply a list of animated property bindings that should be used to resolve
     /// bindings in the current display list.
@@ -252,16 +272,17 @@ impl Transaction {
     }
 
     fn finalize(self) -> (TransactionMsg, Vec<Payload>) {
         (
             TransactionMsg {
                 scene_ops: self.scene_ops,
                 frame_ops: self.frame_ops,
                 resource_updates: self.resource_updates,
+                notifications: self.notifications,
                 use_scene_builder_thread: self.use_scene_builder_thread,
                 generate_frame: self.generate_frame,
                 low_priority: self.low_priority,
             },
             self.payloads,
         )
     }
 
@@ -366,43 +387,49 @@ impl Transaction {
 #[derive(Clone, Deserialize, Serialize)]
 pub struct TransactionMsg {
     pub scene_ops: Vec<SceneMsg>,
     pub frame_ops: Vec<FrameMsg>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub generate_frame: bool,
     pub use_scene_builder_thread: bool,
     pub low_priority: bool,
+
+    #[serde(skip)]
+    pub notifications: Vec<NotificationRequest>,
 }
 
 impl TransactionMsg {
     pub fn is_empty(&self) -> bool {
         !self.generate_frame &&
             self.scene_ops.is_empty() &&
             self.frame_ops.is_empty() &&
-            self.resource_updates.is_empty()
+            self.resource_updates.is_empty() &&
+            self.notifications.is_empty()
     }
 
     // TODO: We only need this for a few RenderApi methods which we should remove.
     pub fn frame_message(msg: FrameMsg) -> Self {
         TransactionMsg {
             scene_ops: Vec::new(),
             frame_ops: vec![msg],
             resource_updates: Vec::new(),
+            notifications: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
             low_priority: false,
         }
     }
 
     pub fn scene_message(msg: SceneMsg) -> Self {
         TransactionMsg {
             scene_ops: vec![msg],
             frame_ops: Vec::new(),
             resource_updates: Vec::new(),
+            notifications: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
             low_priority: false,
         }
     }
 }
 
 #[derive(Clone, Deserialize, Serialize)]
@@ -1135,8 +1162,53 @@ pub trait RenderNotifier: Send {
     fn clone(&self) -> Box<RenderNotifier>;
     fn wake_up(&self);
     fn new_frame_ready(&self, DocumentId, scrolled: bool, composite_needed: bool, render_time_ns: Option<u64>);
     fn external_event(&self, _evt: ExternalEvent) {
         unimplemented!()
     }
     fn shut_down(&self) {}
 }
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Checkpoint {
+    SceneBuilt,
+    FrameBuilt,
+    /// NotificationRequests get notified with this if they get dropped without having been
+    /// notified. This provides the guarantee that if a request is created it will get notified.
+    TransactionDropped,
+}
+
+pub trait NotificationHandler : Send + Sync {
+    fn notify(&self, when: Checkpoint);
+}
+
+#[derive(Clone)]
+pub struct NotificationRequest {
+    handler: Arc<NotificationHandler>,
+    when: Checkpoint,
+    done: bool,
+}
+
+impl NotificationRequest {
+    pub fn new(when: Checkpoint, handler: Arc<NotificationHandler>) -> Self {
+        NotificationRequest {
+            handler,
+            when,
+            done: false,
+        }
+    }
+
+    pub fn when(&self) -> Checkpoint { self.when }
+
+    pub fn notify(mut self) {
+        self.handler.notify(self.when);
+        self.done = true;
+    }
+}
+
+impl Drop for NotificationRequest {
+    fn drop(&mut self) {
+        if !self.done {
+            self.handler.notify(Checkpoint::TransactionDropped);
+        }
+    }
+}
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-02f14d0f333ef125d1abff7b1146039a0ba75f43
+70edb5f8a75ea1e1440ba7984cc42df9eb05ae69