Bug 1477970 - Update webrender to commit 8a4fe66528aa362721e4048aac3cd5abf7faaf2c. r=jrmuizel
authorKartikaya Gupta <kgupta@mozilla.com>
Mon, 30 Jul 2018 09:35:05 -0400
changeset 429249 f065bd870d0f16b1ae251add29eba05ab4742e24
parent 429248 ba66db7432e535f3eb80fd3d3586e64e96275ab3
child 429250 f86485d19c6968da7018bb1ac53becbb92349793
push id105854
push userccoroiu@mozilla.com
push dateMon, 30 Jul 2018 22:03:53 +0000
treeherdermozilla-inbound@129b5ed91c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel
bugs1477970
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 1477970 - Update webrender to commit 8a4fe66528aa362721e4048aac3cd5abf7faaf2c. r=jrmuizel MozReview-Commit-ID: H40i6i2LmAl
gfx/webrender/src/batch.rs
gfx/webrender/src/clip.rs
gfx/webrender/src/clip_node.rs
gfx/webrender/src/clip_scroll_tree.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/freelist.rs
gfx/webrender/src/gpu_types.rs
gfx/webrender/src/hit_test.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/render_task.rs
gfx/webrender/src/resource_cache.rs
gfx/webrender/src/scene.rs
gfx/webrender/src/spatial_node.rs
gfx/webrender_api/src/api.rs
gfx/webrender_bindings/revision.txt
gfx/wrench/src/main.rs
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -492,16 +492,19 @@ impl AlphaBatchBuilder {
                 prim_headers,
             );
         }
 
         // Flush the accumulated plane splits onto the task tree.
         // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
         for poly in splitter.sort(vec3(0.0, 0.0, 1.0)) {
             let prim_index = PrimitiveIndex(poly.anchor);
+            if cfg!(debug_assertions) && ctx.prim_store.chase_id == Some(prim_index) {
+                println!("\t\tsplit polygon {:?}", poly.points);
+            }
             debug!("process sorted poly {:?} {:?}", prim_index, poly.points);
             let pp = &poly.points;
             let gpu_blocks = [
                 [pp[0].x as f32, pp[0].y as f32, pp[0].z as f32, pp[1].x as f32].into(),
                 [pp[1].y as f32, pp[1].z as f32, pp[2].x as f32, pp[2].y as f32].into(),
                 [pp[2].z as f32, pp[3].x as f32, pp[3].y as f32, pp[3].z as f32].into(),
             ];
             let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
@@ -649,24 +652,28 @@ impl AlphaBatchBuilder {
             local_rect: prim_metadata.local_rect,
             local_clip_rect: prim_metadata.combined_local_clip_rect,
             task_address,
             specific_prim_address: prim_cache_address,
             clip_task_address,
             transform_id,
         };
 
+        if cfg!(debug_assertions) && ctx.prim_store.chase_id == Some(prim_index) {
+            println!("\ttask target {:?}", self.target_rect);
+            println!("\t{:?}", prim_header);
+        }
+
         match prim_metadata.prim_kind {
             PrimitiveKind::Brush => {
                 let brush = &ctx.prim_store.cpu_brushes[prim_metadata.cpu_prim_index.0];
 
                 match brush.kind {
                     BrushKind::Picture { pic_index, .. } => {
-                        let picture =
-                            &ctx.prim_store.pictures[pic_index.0];
+                        let picture = &ctx.prim_store.pictures[pic_index.0];
 
                         // 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());
 
@@ -1051,18 +1058,23 @@ impl AlphaBatchBuilder {
                             prim_headers,
                         );
                     }
                     _ => {
                         if let Some((batch_kind, textures, user_data)) = brush.get_batch_params(
                                 ctx.resource_cache,
                                 gpu_cache,
                                 deferred_resolves,
+                                ctx.prim_store.chase_id == Some(prim_index),
                         ) {
                             let prim_header_index = prim_headers.push(&prim_header, user_data);
+                            if cfg!(debug_assertions) && ctx.prim_store.chase_id == Some(prim_index) {
+                                println!("\t{:?} {:?}, task relative bounds {:?}",
+                                    batch_kind, prim_header_index, task_relative_bounding_rect);
+                            }
 
                             self.add_brush_to_batch(
                                 brush,
                                 prim_metadata,
                                 batch_kind,
                                 specified_blend_mode,
                                 non_segmented_blend_mode,
                                 textures,
@@ -1380,16 +1392,17 @@ impl BrushPrimitive {
         }
     }
 
     fn get_batch_params(
         &self,
         resource_cache: &ResourceCache,
         gpu_cache: &mut GpuCache,
         deferred_resolves: &mut Vec<DeferredResolve>,
+        is_chased: bool,
     ) -> Option<(BrushBatchKind, BatchTextures, [i32; 3])> {
         match self.kind {
             BrushKind::Image { request, ref source, .. } => {
                 let cache_item = match *source {
                     ImageSource::Default => {
                         resolve_image(
                             request,
                             resource_cache,
@@ -1401,16 +1414,19 @@ impl BrushPrimitive {
                         let rt_handle = handle
                             .as_ref()
                             .expect("bug: render task handle not allocated");
                         let rt_cache_entry = resource_cache
                             .get_cached_render_task(rt_handle);
                         resource_cache.get_texture_cache_item(&rt_cache_entry.handle)
                     }
                 };
+                if cfg!(debug_assertions) && is_chased {
+                    println!("\tsource {:?}", cache_item);
+                }
 
                 if cache_item.texture_id == SourceTexture::Invalid {
                     None
                 } else {
                     let textures = BatchTextures::color(cache_item.texture_id);
 
                     Some((
                         BrushBatchKind::Image(get_buffer_kind(cache_item.texture_id)),
@@ -1746,17 +1762,17 @@ impl ClipBatcher {
 
     pub fn add_clip_region(
         &mut self,
         task_address: RenderTaskAddress,
         clip_data_address: GpuCacheAddress,
     ) {
         let instance = ClipMaskInstance {
             render_task_address: task_address,
-            transform_id: TransformPaletteId::identity(),
+            transform_id: TransformPaletteId::IDENTITY,
             segment: 0,
             clip_data_address,
             resource_address: GpuCacheAddress::invalid(),
         };
 
         self.rectangles.push(instance);
     }
 
@@ -1767,26 +1783,24 @@ impl ClipBatcher {
         coordinate_system_id: CoordinateSystemId,
         resource_cache: &ResourceCache,
         gpu_cache: &GpuCache,
         clip_store: &ClipStore,
         transforms: &TransformPalette,
     ) {
         let mut coordinate_system_id = coordinate_system_id;
         for work_item in clips.iter() {
+            let info = clip_store.get(work_item.clip_sources_index);
             let instance = ClipMaskInstance {
                 render_task_address: task_address,
-                transform_id: transforms.get_id(work_item.spatial_node_index),
+                transform_id: transforms.get_id(info.spatial_node_index),
                 segment: 0,
                 clip_data_address: GpuCacheAddress::invalid(),
                 resource_address: GpuCacheAddress::invalid(),
             };
-            let info = clip_store
-                .get_opt(&work_item.clip_sources)
-                .expect("bug: clip handle should be valid");
 
             for &(ref source, ref handle) in &info.clips {
                 let gpu_address = gpu_cache.get_address(handle);
 
                 match *source {
                     ClipSource::Image(ref mask) => {
                         if let Ok(cache_item) = resource_cache.get_cached_image(
                             ImageRequest {
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -4,32 +4,61 @@
 
 use api::{BorderRadius, ClipMode, ComplexClipRegion, DeviceIntRect, DevicePixelScale, ImageMask};
 use api::{ImageRendering, LayoutRect, LayoutSize, LayoutPoint, LayoutVector2D, LocalClip};
 use api::{BoxShadowClipMode, LayoutToWorldScale, LineOrientation, LineStyle};
 use border::{ensure_no_corner_overlap};
 use box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey};
 use clip_scroll_tree::{ClipChainIndex, CoordinateSystemId, SpatialNodeIndex};
 use ellipse::Ellipse;
-use freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use gpu_types::BoxShadowStretchMode;
 use prim_store::{ClipData, ImageMaskData};
 use render_task::to_cache_size;
 use resource_cache::{ImageRequest, ResourceCache};
 use util::{LayoutToWorldFastTransform, MaxRect, calculate_screen_bounding_rect};
-use util::{extract_inner_rect_safe, pack_as_float};
+use util::{extract_inner_rect_safe, pack_as_float, recycle_vec};
 use std::sync::Arc;
 
-#[derive(Debug)]
-pub enum ClipStoreMarker {}
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipSourcesIndex(usize);
+
+pub struct ClipStore {
+    clip_sources: Vec<ClipSources>,
+}
+
+impl ClipStore {
+    pub fn new() -> ClipStore {
+        ClipStore {
+            clip_sources: Vec::new(),
+        }
+    }
 
-pub type ClipStore = FreeList<ClipSources, ClipStoreMarker>;
-pub type ClipSourcesHandle = FreeListHandle<ClipStoreMarker>;
-pub type ClipSourcesWeakHandle = WeakFreeListHandle<ClipStoreMarker>;
+    pub fn recycle(self) -> ClipStore {
+        ClipStore {
+            clip_sources: recycle_vec(self.clip_sources),
+        }
+    }
+
+    pub fn insert(&mut self, clip_sources: ClipSources) -> ClipSourcesIndex {
+        let index = ClipSourcesIndex(self.clip_sources.len());
+        self.clip_sources.push(clip_sources);
+        index
+    }
+
+    pub fn get(&self, index: ClipSourcesIndex) -> &ClipSources {
+        &self.clip_sources[index.0]
+    }
+
+    pub fn get_mut(&mut self, index: ClipSourcesIndex) -> &mut ClipSources {
+        &mut self.clip_sources[index.0]
+    }
+}
 
 #[derive(Debug)]
 pub struct LineDecorationClipSource {
     rect: LayoutRect,
     style: LineStyle,
     orientation: LineOrientation,
     wavy_line_thickness: f32,
 }
@@ -86,38 +115,16 @@ impl ClipRegion {
 pub enum ClipSource {
     Rectangle(LayoutRect, ClipMode),
     RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
     Image(ImageMask),
     BoxShadow(BoxShadowClipSource),
     LineDecoration(LineDecorationClipSource),
 }
 
-impl From<ClipRegion> for ClipSources {
-    fn from(region: ClipRegion) -> ClipSources {
-        let mut clips = Vec::new();
-
-        if let Some(info) = region.image_mask {
-            clips.push(ClipSource::Image(info));
-        }
-
-        clips.push(ClipSource::Rectangle(region.main, ClipMode::Clip));
-
-        for complex in region.complex_clips {
-            clips.push(ClipSource::new_rounded_rect(
-                complex.rect,
-                complex.radii,
-                complex.mode,
-            ));
-        }
-
-        ClipSources::new(clips)
-    }
-}
-
 impl ClipSource {
     pub fn new_rounded_rect(
         rect: LayoutRect,
         mut radii: BorderRadius,
         clip_mode: ClipMode
     ) -> ClipSource {
         if radii.is_zero() {
             ClipSource::Rectangle(rect, clip_mode)
@@ -275,20 +282,24 @@ impl ClipSource {
 
 #[derive(Debug)]
 pub struct ClipSources {
     pub clips: Vec<(ClipSource, GpuCacheHandle)>,
     pub local_inner_rect: LayoutRect,
     pub local_outer_rect: Option<LayoutRect>,
     pub only_rectangular_clips: bool,
     pub has_image_or_line_decoration_clip: bool,
+    pub spatial_node_index: SpatialNodeIndex,
 }
 
 impl ClipSources {
-    pub fn new(clips: Vec<ClipSource>) -> Self {
+    pub fn new(
+        clips: Vec<ClipSource>,
+        spatial_node_index: SpatialNodeIndex,
+    ) -> Self {
         let (local_inner_rect, local_outer_rect) = Self::calculate_inner_and_outer_rects(&clips);
 
         let has_image_or_line_decoration_clip =
             clips.iter().any(|clip| clip.is_image_or_line_decoration_clip());
         let only_rectangular_clips =
             !has_image_or_line_decoration_clip && clips.iter().all(|clip| clip.is_rect());
         let clips = clips
             .into_iter()
@@ -296,19 +307,43 @@ impl ClipSources {
             .collect();
 
         ClipSources {
             clips,
             local_inner_rect,
             local_outer_rect,
             only_rectangular_clips,
             has_image_or_line_decoration_clip,
+            spatial_node_index,
         }
     }
 
+    pub fn from_region(
+        region: ClipRegion,
+        spatial_node_index: SpatialNodeIndex,
+    ) -> ClipSources {
+        let mut clips = Vec::new();
+
+        if let Some(info) = region.image_mask {
+            clips.push(ClipSource::Image(info));
+        }
+
+        clips.push(ClipSource::Rectangle(region.main, ClipMode::Clip));
+
+        for complex in region.complex_clips {
+            clips.push(ClipSource::new_rounded_rect(
+                complex.rect,
+                complex.radii,
+                complex.mode,
+            ));
+        }
+
+        ClipSources::new(clips, spatial_node_index)
+    }
+
     pub fn clips(&self) -> &[(ClipSource, GpuCacheHandle)] {
         &self.clips
     }
 
     fn calculate_inner_and_outer_rects(clips: &Vec<ClipSource>) -> (LayoutRect, Option<LayoutRect>) {
         if clips.is_empty() {
             return (LayoutRect::zero(), None);
         }
@@ -642,13 +677,12 @@ impl Iterator for ClipChainNodeIter {
         previous
     }
 }
 
 #[derive(Debug, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipWorkItem {
-    pub spatial_node_index: SpatialNodeIndex,
-    pub clip_sources: ClipSourcesWeakHandle,
+    pub clip_sources_index: ClipSourcesIndex,
     pub coordinate_system_id: CoordinateSystemId,
 }
 
--- a/gfx/webrender/src/clip_node.rs
+++ b/gfx/webrender/src/clip_node.rs
@@ -1,70 +1,50 @@
 /* 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::DevicePixelScale;
-use clip::{ClipChain, ClipChainNode, ClipSourcesHandle, ClipStore, ClipWorkItem};
-use clip_scroll_tree::{ClipChainIndex, SpatialNodeIndex};
+use clip::{ClipChain, ClipChainNode, ClipSourcesIndex, ClipStore, ClipWorkItem};
+use clip_scroll_tree::{ClipChainIndex};
 use gpu_cache::GpuCache;
 use resource_cache::ResourceCache;
 use spatial_node::SpatialNode;
 
 #[derive(Debug)]
 pub struct ClipNode {
-    /// The node that determines how this clip node is positioned.
-    pub spatial_node: SpatialNodeIndex,
-
     /// A handle to this clip nodes clips in the ClipStore.
-    pub handle: Option<ClipSourcesHandle>,
+    pub clip_sources_index: ClipSourcesIndex,
 
     /// An index to a ClipChain defined by this ClipNode's hiearchy in the display
     /// list.
     pub clip_chain_index: ClipChainIndex,
 
     /// The index of the parent ClipChain of this node's hiearchical ClipChain.
     pub parent_clip_chain_index: ClipChainIndex,
 
     /// A copy of the ClipChainNode this node would produce. We need to keep a copy,
     /// because the ClipChain may not contain our node if is optimized out, but API
     /// 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::NO_CLIP,
-        parent_clip_chain_index: ClipChainIndex::NO_CLIP,
-        clip_chain_node: None,
-    };
-
-    pub fn empty() -> ClipNode {
-        ClipNode::EMPTY
-    }
-
     pub fn update(
         &mut self,
-        spatial_node: &SpatialNode,
         device_pixel_scale: DevicePixelScale,
         clip_store: &mut ClipStore,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         clip_chains: &mut [ClipChain],
+        spatial_nodes: &[SpatialNode],
     ) {
-        let (clip_sources, weak_handle) = match self.handle {
-            Some(ref handle) => (clip_store.get_mut(handle), handle.weak()),
-            None => {
-                warn!("Tried to process an empty clip node");
-                return;
-            }
-        };
+        let clip_sources = clip_store.get_mut(self.clip_sources_index);
         clip_sources.update(gpu_cache, resource_cache, device_pixel_scale);
+        let spatial_node = &spatial_nodes[clip_sources.spatial_node_index.0];
 
         let (screen_inner_rect, screen_outer_rect) = clip_sources.get_screen_bounds(
             &spatial_node.world_content_transform,
             device_pixel_scale,
             None,
         );
 
         // All clipping SpatialNodes should have outer rectangles, because they never
@@ -72,18 +52,17 @@ impl ClipNode {
         // Rectangle ClipSource.
         let screen_outer_rect = screen_outer_rect
             .expect("Clipping node didn't have outer rect.");
         let local_outer_rect = clip_sources.local_outer_rect
             .expect("Clipping node didn't have outer rect.");
 
         let new_node = ClipChainNode {
             work_item: ClipWorkItem {
-                spatial_node_index: self.spatial_node,
-                clip_sources: weak_handle,
+                clip_sources_index: self.clip_sources_index,
                 coordinate_system_id: spatial_node.coordinate_system_id,
             },
             local_clip_rect: spatial_node
                 .coordinate_system_relative_transform
                 .transform_rect(&local_outer_rect)
                 .expect("clip node transform is not valid"),
             screen_outer_rect,
             screen_inner_rect,
--- a/gfx/webrender/src/clip_scroll_tree.rs
+++ b/gfx/webrender/src/clip_scroll_tree.rs
@@ -1,16 +1,16 @@
 /* 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::{DeviceIntRect, DevicePixelScale, ExternalScrollId, LayoutPoint, LayoutRect, LayoutVector2D};
 use api::{PipelineId, ScrollClamping, ScrollLocation, ScrollNodeState};
 use api::{LayoutSize, LayoutTransform, PropertyBinding, ScrollSensitivity, WorldPoint};
-use clip::{ClipChain, ClipSourcesHandle, ClipStore};
+use clip::{ClipChain, ClipSourcesIndex, ClipStore};
 use clip_node::ClipNode;
 use gpu_cache::GpuCache;
 use gpu_types::TransformPalette;
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintTree, PrintTreePrinter};
 use resource_cache::ResourceCache;
 use scene::SceneProperties;
 use spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo};
@@ -251,24 +251,23 @@ impl ClipScrollTree {
             root_reference_frame_index,
             &mut state,
             &mut next_coordinate_system_id,
             &mut transform_palette,
             scene_properties,
         );
 
         for clip_node in &mut self.clip_nodes {
-            let spatial_node = &self.spatial_nodes[clip_node.spatial_node.0];
             clip_node.update(
-                spatial_node,
                 device_pixel_scale,
                 clip_store,
                 resource_cache,
                 gpu_cache,
                 &mut self.clip_chains,
+                &self.spatial_nodes,
             );
         }
         self.build_clip_chains(screen_rect);
 
         transform_palette
     }
 
     fn update_node(
@@ -350,139 +349,108 @@ impl ClipScrollTree {
             if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&external_id) {
                 node.set_scroll_origin(&offset, clamping);
             }
         }
     }
 
     pub fn add_clip_node(
         &mut self,
-        index: ClipNodeIndex,
         parent_clip_chain_index: ClipChainIndex,
-        spatial_node: SpatialNodeIndex,
-        handle: ClipSourcesHandle,
-    )  -> ClipChainIndex {
+        clip_sources_index: ClipSourcesIndex,
+    ) -> (ClipNodeIndex, ClipChainIndex) {
         let clip_chain_index = self.allocate_clip_chain();
         let node = ClipNode {
             parent_clip_chain_index,
-            spatial_node,
-            handle: Some(handle),
+            clip_sources_index,
             clip_chain_index,
             clip_chain_node: None,
         };
-        self.push_clip_node(node, index);
-        clip_chain_index
+        let node_index = self.push_clip_node(node);
+        (node_index, clip_chain_index)
     }
 
     pub fn add_scroll_frame(
         &mut self,
-        index: SpatialNodeIndex,
         parent_index: SpatialNodeIndex,
         external_id: Option<ExternalScrollId>,
         pipeline_id: PipelineId,
         frame_rect: &LayoutRect,
         content_size: &LayoutSize,
         scroll_sensitivity: ScrollSensitivity,
-    ) {
+    ) -> SpatialNodeIndex {
         let node = SpatialNode::new_scroll_frame(
             pipeline_id,
             parent_index,
             external_id,
             frame_rect,
             content_size,
             scroll_sensitivity,
         );
-        self.add_spatial_node(node, index);
+        self.add_spatial_node(node)
     }
 
     pub fn add_reference_frame(
         &mut self,
-        index: SpatialNodeIndex,
         parent_index: Option<SpatialNodeIndex>,
         source_transform: Option<PropertyBinding<LayoutTransform>>,
         source_perspective: Option<LayoutTransform>,
         origin_in_parent_reference_frame: LayoutVector2D,
         pipeline_id: PipelineId,
-    ) {
+    ) -> SpatialNodeIndex {
         let node = SpatialNode::new_reference_frame(
             parent_index,
             source_transform,
             source_perspective,
             origin_in_parent_reference_frame,
             pipeline_id,
         );
-        self.add_spatial_node(node, index);
+        self.add_spatial_node(node)
     }
 
     pub fn add_sticky_frame(
         &mut self,
-        index: SpatialNodeIndex,
         parent_index: SpatialNodeIndex,
         sticky_frame_info: StickyFrameInfo,
         pipeline_id: PipelineId,
-    ) {
+    ) -> SpatialNodeIndex {
         let node = SpatialNode::new_sticky_frame(
             parent_index,
             sticky_frame_info,
             pipeline_id,
         );
-        self.add_spatial_node(node, index);
+        self.add_spatial_node(node)
     }
 
     pub fn add_clip_chain_descriptor(
         &mut self,
         parent: Option<ClipChainIndex>,
         clips: Vec<ClipNodeIndex>
     ) -> ClipChainIndex {
         let index = self.allocate_clip_chain();
         self.clip_chains_descriptors.push(ClipChainDescriptor { index, parent, clips });
         index
     }
 
-    pub fn push_clip_node(&mut self, node: ClipNode, index: ClipNodeIndex) {
-        if index.0 == self.clip_nodes.len() {
-            self.clip_nodes.push(node);
-            return;
-        }
-
-        if let Some(empty_node) = self.clip_nodes.get_mut(index.0) {
-            *empty_node = node;
-            return
-        }
-
-        let length_to_reserve = index.0 + 1 - self.clip_nodes.len();
-        self.clip_nodes.reserve_exact(length_to_reserve);
-
-        // We would like to use `Vec::resize` here, but the Clone trait is not supported
-        // for ClipNodes. We can fix this either when support is added for something like
-        // `Vec::resize_default`.
-        let length_to_extend = self.clip_nodes.len() .. index.0;
-        self.clip_nodes.extend(length_to_extend.map(|_| ClipNode::empty()));
+    pub fn push_clip_node(&mut self, node: ClipNode) -> ClipNodeIndex {
+        let index = ClipNodeIndex(self.clip_nodes.len());
         self.clip_nodes.push(node);
+        index
     }
 
-    pub fn add_spatial_node(&mut self, node: SpatialNode, index: SpatialNodeIndex) {
+    pub fn add_spatial_node(&mut self, node: SpatialNode) -> SpatialNodeIndex {
+        let index = SpatialNodeIndex(self.spatial_nodes.len());
+
         // When the parent node is None this means we are adding the root.
         if let Some(parent_index) = node.parent {
             self.spatial_nodes[parent_index.0].add_child(index);
         }
 
-        if index.0 == self.spatial_nodes.len() {
-            self.spatial_nodes.push(node);
-            return;
-        }
-
-        if let Some(empty_node) = self.spatial_nodes.get_mut(index.0) {
-            *empty_node = node;
-            return
-        }
-
-        debug_assert!(index.0 > self.spatial_nodes.len() - 1);
-        self.spatial_nodes.resize(index.0, SpatialNode::empty());
         self.spatial_nodes.push(node);
+        index
     }
 
     pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
         self.pipelines_to_discard.insert(pipeline_id);
     }
 
     fn print_node<T: PrintTreePrinter>(
         &self,
@@ -503,17 +471,16 @@ impl ClipScrollTree {
                 pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
                 pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
                 pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
             }
             SpatialNodeType::ReferenceFrame(ref info) => {
                 pt.new_level(format!("ReferenceFrame {:?}", info.resolved_transform));
                 pt.add_item(format!("index: {:?}", index));
             }
-            SpatialNodeType::Empty => unreachable!("Empty node remaining in ClipScrollTree."),
         }
 
         pt.add_item(format!("world_viewport_transform: {:?}", node.world_viewport_transform));
         pt.add_item(format!("world_content_transform: {:?}", node.world_content_transform));
         pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
 
         for child_index in &node.children {
             self.print_node(*child_index, pt, clip_store);
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -8,17 +8,17 @@ use api::{ClipId, ColorF, ComplexClipReg
 use api::{DevicePixelScale, DeviceUintRect, DisplayItemRef, ExtendMode, ExternalScrollId};
 use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, GlyphRasterSpace, GradientStop};
 use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, LayoutPoint};
 use api::{LayoutPrimitiveInfo, LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D};
 use api::{LineOrientation, LineStyle, LocalClip, NinePatchBorderSource, PipelineId};
 use api::{PropertyBinding, ReferenceFrame, RepeatMode, ScrollFrameDisplayItem, ScrollSensitivity};
 use api::{Shadow, SpecificDisplayItem, StackingContext, StickyFrameDisplayItem, TexelRect};
 use api::{TransformStyle, YuvColorSpace, YuvData};
-use clip::{ClipRegion, ClipSource, ClipSources, ClipStore};
+use clip::{ClipRegion, ClipSource, ClipSources, ClipSourcesIndex, ClipStore};
 use clip_scroll_tree::{ClipChainIndex, ClipNodeIndex, ClipScrollTree, SpatialNodeIndex};
 use euclid::vec2;
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
 use gpu_cache::GpuCacheHandle;
 use gpu_types::BrushFlags;
 use hit_test::{HitTestingItem, HitTestingRun};
 use image::simplify_repeated_primitive;
@@ -28,137 +28,86 @@ use prim_store::{BrushClipMaskKind, Brus
 use prim_store::{EdgeAaSegmentMask, ImageSource};
 use prim_store::{BorderSource, BrushSegment, PictureIndex, PrimitiveContainer, PrimitiveIndex, PrimitiveStore};
 use prim_store::{OpacityBinding, ScrollNodeAndClipChain, TextRunPrimitiveCpu};
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, ScenePipeline, StackingContextHelpers};
 use scene_builder::{BuiltScene, SceneRequest};
 use spatial_node::{SpatialNodeType, StickyFrameInfo};
-use std::{f32, mem, usize};
+use std::{f32, mem};
 use tiling::{CompositeOps, ScrollbarPrimitive};
 use util::{MaxRect, RectHelpers, recycle_vec};
 
 static DEFAULT_SCROLLBAR_COLOR: ColorF = ColorF {
     r: 0.3,
     g: 0.3,
     b: 0.3,
     a: 0.6,
 };
 
-#[derive(Clone, Copy)]
-pub struct PipelineOffset {
-    pipeline: PipelineId,
-    spatial_offset: usize,
-    clip_offset: usize,
-}
-
 /// A data structure that keeps track of mapping between API ClipIds and the indices used
 /// internally in the ClipScrollTree to avoid having to do HashMap lookups. ClipIdToIndexMapper is
-/// responsible for mapping both ClipId to ClipChainIndex and ClipId to SpatialNodeIndex.  We
-/// also include two small LRU caches. Currently the caches are small (1 entry), but in the future
-/// we could use uluru here to do something more involved.
+/// responsible for mapping both ClipId to ClipChainIndex and ClipId to SpatialNodeIndex.
 #[derive(Default)]
 pub struct ClipIdToIndexMapper {
-    /// A map which converts a ClipId for a clipping node or an API-defined ClipChain into
-    /// a ClipChainIndex, which is the index used internally in the ClipScrollTree to
-    /// identify ClipChains.
     clip_chain_map: FastHashMap<ClipId, ClipChainIndex>,
-
-    /// The last mapped ClipChainIndex, used to avoid having to do lots of consecutive
-    /// HashMap lookups.
-    cached_clip_chain_index: Option<(ClipId, ClipChainIndex)>,
-
-    /// The offset in the ClipScrollTree's array of SpatialNodes and ClipNodes for a particular
-    /// pipeline.  This is used to convert ClipIds into SpatialNodeIndex or ClipNodeIndex.
-    pipeline_offsets: FastHashMap<PipelineId, PipelineOffset>,
-
-    /// The last mapped pipeline offset for this mapper. This is used to avoid having to
-    /// consult `pipeline_offsets` repeatedly when flattening the display list.
-    cached_pipeline_offset: Option<PipelineOffset>,
-
-    /// The next available pipeline offset for ClipNodeIndex. When we encounter a pipeline
-    /// we will use this value and increment it by the total number of clip nodes in the
-    /// pipeline's display list.
-    next_available_clip_offset: usize,
-
-    /// The next available pipeline offset for SpatialNodeIndex. When we encounter a pipeline
-    /// we will use this value and increment it by the total number of spatial nodes in the
-    /// pipeline's display list.
-    next_available_spatial_offset: usize,
+    clip_node_map: FastHashMap<ClipId, ClipNodeIndex>,
+    spatial_node_map: FastHashMap<ClipId, SpatialNodeIndex>,
 }
 
 impl ClipIdToIndexMapper {
     pub fn add_clip_chain(&mut self, id: ClipId, index: ClipChainIndex) {
-        debug_assert!(!self.clip_chain_map.contains_key(&id));
-        self.clip_chain_map.insert(id, index);
+        let _old_value = self.clip_chain_map.insert(id, index);
+        debug_assert!(_old_value.is_none());
     }
 
     pub fn map_to_parent_clip_chain(&mut self, id: ClipId, parent_id: &ClipId) {
         let parent_chain_index = self.get_clip_chain_index(parent_id);
         self.add_clip_chain(id, parent_chain_index);
     }
 
-    pub fn get_clip_chain_index(&mut self, id: &ClipId) -> ClipChainIndex {
-        match self.cached_clip_chain_index {
-            Some((cached_id, cached_clip_chain_index)) if cached_id == *id =>
-                return cached_clip_chain_index,
-            _ => {}
-        }
+    pub fn map_spatial_node(&mut self, id: ClipId, index: SpatialNodeIndex) {
+        let _old_value = self.spatial_node_map.insert(id, index);
+        debug_assert!(_old_value.is_none());
+    }
 
+    pub fn map_clip_node(&mut self, id: ClipId, index: ClipNodeIndex) {
+        let _old_value = self.clip_node_map.insert(id, index);
+        debug_assert!(_old_value.is_none());
+    }
+
+    pub fn get_clip_chain_index(&self, id: &ClipId) -> ClipChainIndex {
         self.clip_chain_map[id]
     }
 
-    pub fn get_clip_chain_index_and_cache_result(&mut self, id: &ClipId) -> ClipChainIndex {
-        let index = self.get_clip_chain_index(id);
-        self.cached_clip_chain_index = Some((*id, index));
-        index
-    }
-
-    pub fn initialize_for_pipeline(&mut self, pipeline: &ScenePipeline) {
-        debug_assert!(!self.pipeline_offsets.contains_key(&pipeline.pipeline_id));
-        self.pipeline_offsets.insert(
-            pipeline.pipeline_id,
-            PipelineOffset {
-                pipeline: pipeline.pipeline_id,
-                spatial_offset: self.next_available_spatial_offset,
-                clip_offset: self.next_available_clip_offset,
-            }
-        );
-
-        self.next_available_clip_offset += pipeline.display_list.total_clip_nodes();
-        self.next_available_spatial_offset += pipeline.display_list.total_spatial_nodes();
-    }
-
-    pub fn get_pipeline_offet<'a>(&'a mut self, id: PipelineId) -> &'a PipelineOffset {
-        match self.cached_pipeline_offset {
-            Some(ref offset) if offset.pipeline == id => offset,
-            _ => {
-                let offset = &self.pipeline_offsets[&id];
-                self.cached_pipeline_offset = Some(*offset);
-                offset
-            }
-        }
-    }
-
-    pub fn get_clip_node_index(&mut self, id: ClipId) -> ClipNodeIndex {
+    pub fn get_clip_node_index(&self, id: ClipId) -> ClipNodeIndex {
         match id {
-            ClipId::Clip(index, pipeline_id) => {
-                let pipeline_offset = self.get_pipeline_offet(pipeline_id);
-                ClipNodeIndex(pipeline_offset.clip_offset + index)
+            ClipId::Clip(..) => {
+                self.clip_node_map[&id]
             }
             ClipId::Spatial(..) => {
                 // We could theoretically map back to the containing clip node with the current
                 // design, but we will eventually fully separate out clipping from spatial nodes
                 // in the display list. We don't ever need to do this anyway.
                 panic!("Tried to use positioning node as clip node.");
             }
             ClipId::ClipChain(_) => panic!("Tried to use ClipChain as scroll node."),
         }
     }
+
+    pub fn get_spatial_node_index(&self, id: ClipId) -> SpatialNodeIndex {
+        match id {
+            ClipId::Clip(..) |
+            ClipId::Spatial(..) => {
+                self.spatial_node_map[&id]
+            }
+            ClipId::ClipChain(_) => panic!("Tried to use ClipChain as scroll node."),
+        }
+    }
 }
 
 /// A structure that converts a serialized display list into a form that WebRender
 /// can use to later build a frame. This structure produces a FrameBuilder. Public
 /// members are typically those that are destructured into the FrameBuilder.
 pub struct DisplayListFlattener<'a> {
     /// The scene that we are currently flattening.
     scene: &'a Scene,
@@ -243,17 +192,16 @@ impl<'a> DisplayListFlattener<'a> {
             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,
             &root_pipeline.content_size,
         );
         flattener.setup_viewport_offset(view.inner_rect, view.accumulated_scale_factor());
         flattener.flatten_root(root_pipeline, &root_pipeline.viewport_size);
 
@@ -396,23 +344,22 @@ impl<'a> DisplayListFlattener<'a> {
         let sticky_frame_info = StickyFrameInfo::new(
             frame_rect,
             info.margins,
             info.vertical_offset_bounds,
             info.horizontal_offset_bounds,
             info.previously_applied_offset,
         );
 
-        let index = self.get_spatial_node_index_for_clip_id(info.id);
-        self.clip_scroll_tree.add_sticky_frame(
-            index,
+        let index = self.clip_scroll_tree.add_sticky_frame(
             clip_and_scroll.spatial_node_index, /* parent id */
             sticky_frame_info,
             info.id.pipeline_id(),
         );
+        self.id_to_index_mapper.map_spatial_node(info.id, index);
         self.id_to_index_mapper.map_to_parent_clip_chain(info.id, parent_id);
     }
 
     fn flatten_scroll_frame(
         &mut self,
         item: &DisplayItemRef,
         info: &ScrollFrameDisplayItem,
         pipeline_id: PipelineId,
@@ -527,18 +474,16 @@ impl<'a> DisplayListFlattener<'a> {
         let pipeline = match self.scene.pipelines.get(&iframe_pipeline_id) {
             Some(pipeline) => pipeline,
             None => {
                 debug_assert!(info.ignore_missing_pipeline);
                 return
             },
         };
 
-        self.id_to_index_mapper.initialize_for_pipeline(pipeline);
-
         //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
             ),
@@ -786,33 +731,39 @@ impl<'a> DisplayListFlattener<'a> {
             }
             SpecificDisplayItem::PopAllShadows => {
                 self.pop_all_shadows();
             }
         }
         None
     }
 
+    fn add_clip_sources(
+        &mut self,
+        clip_sources: Vec<ClipSource>,
+        spatial_node_index: SpatialNodeIndex,
+    ) -> Option<ClipSourcesIndex> {
+        if clip_sources.is_empty() {
+            None
+        } else {
+            Some(self.clip_store.insert(ClipSources::new(clip_sources, spatial_node_index)))
+        }
+    }
+
     /// Create a primitive and add it to the prim store. This method doesn't
     /// add the primitive to the draw list, so can be used for creating
     /// sub-primitives.
     pub fn create_primitive(
         &mut self,
         info: &LayoutPrimitiveInfo,
-        clip_sources: Vec<ClipSource>,
+        clip_sources: Option<ClipSourcesIndex>,
         container: PrimitiveContainer,
     ) -> PrimitiveIndex {
         let stacking_context = self.sc_stack.last().expect("bug: no stacking context!");
 
-        let clip_sources = if clip_sources.is_empty() {
-            None
-        } else {
-            Some(self.clip_store.insert(ClipSources::new(clip_sources)))
-        };
-
         self.prim_store.add_primitive(
             &info.rect,
             &info.clip_rect,
             info.is_backface_visible && stacking_context.is_backface_visible,
             clip_sources,
             info.tag,
             container,
         )
@@ -872,32 +823,40 @@ impl<'a> DisplayListFlattener<'a> {
                 info.rect = info.rect.translate(&shadow.offset);
                 info.clip_rect = info.clip_rect.translate(&shadow.offset);
 
                 // Offset any local clip sources by the shadow offset.
                 let clip_sources: Vec<ClipSource> = clip_sources
                     .iter()
                     .map(|cs| cs.offset(&shadow.offset))
                     .collect();
+                let clip_sources = self.add_clip_sources(
+                    clip_sources,
+                    clip_and_scroll.spatial_node_index,
+                );
 
                 // Construct and add a primitive for the given shadow.
                 let shadow_prim_index = self.create_primitive(
                     &info,
                     clip_sources,
                     container.create_shadow(shadow),
                 );
 
                 // Add the new primitive to the shadow picture.
                 let shadow_pic = &mut self.prim_store.pictures[shadow_pic_index.0];
                 shadow_pic.add_primitive(shadow_prim_index, clip_and_scroll);
             }
             self.shadow_stack = shadow_stack;
         }
 
         if container.is_visible() {
+            let clip_sources = self.add_clip_sources(
+                clip_sources,
+                clip_and_scroll.spatial_node_index,
+            );
             let prim_index = self.create_primitive(info, clip_sources, container);
             if cfg!(debug_assertions) && ChasePrimitive::LocalRect(info.rect) == self.config.chase_primitive {
                 println!("Chasing {:?}", prim_index);
                 self.prim_store.chase_id = Some(prim_index);
             }
             self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
             self.add_primitive_to_draw_list(prim_index, clip_and_scroll);
         }
@@ -914,17 +873,17 @@ impl<'a> DisplayListFlattener<'a> {
         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::NO_CLIP,
         };
         let clip_and_scroll = ScrollNodeAndClipChain::new(
-            self.get_spatial_node_index_for_clip_id(spatial_node),
+            self.id_to_index_mapper.get_spatial_node_index(spatial_node),
             clip_chain_id
         );
 
         // Construct the necessary set of Picture primitives
         // to draw this stacking context.
         let current_reference_frame_index = self.current_reference_frame_index();
 
         // An arbitrary large clip rect. For now, we don't
@@ -1214,27 +1173,26 @@ impl<'a> DisplayListFlattener<'a> {
         &mut self,
         reference_frame_id: ClipId,
         parent_id: Option<ClipId>,
         pipeline_id: PipelineId,
         source_transform: Option<PropertyBinding<LayoutTransform>>,
         source_perspective: Option<LayoutTransform>,
         origin_in_parent_reference_frame: LayoutVector2D,
     ) -> SpatialNodeIndex {
-        let index = self.get_spatial_node_index_for_clip_id(reference_frame_id);
-        let parent_index = parent_id.map(|id| self.get_spatial_node_index_for_clip_id(id));
-        self.clip_scroll_tree.add_reference_frame(
-            index,
+        let parent_index = parent_id.map(|id| self.id_to_index_mapper.get_spatial_node_index(id));
+        let index = self.clip_scroll_tree.add_reference_frame(
             parent_index,
             source_transform,
             source_perspective,
             origin_in_parent_reference_frame,
             pipeline_id,
         );
         self.reference_frame_stack.push((reference_frame_id, index));
+        self.id_to_index_mapper.map_spatial_node(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::NO_CLIP),
         }
         index
     }
@@ -1284,54 +1242,52 @@ 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 parent_clip_chain_index = self.id_to_index_mapper.get_clip_chain_index(&parent_id);
+        let spatial_node = self.id_to_index_mapper.get_spatial_node_index(parent_id);
+
+        let clip_sources = ClipSources::from_region(clip_region, spatial_node);
         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,
+        let (node_index, clip_chain_index) = self.clip_scroll_tree.add_clip_node(
             parent_clip_chain_index,
-            spatial_node,
             handle,
         );
+        self.id_to_index_mapper.map_spatial_node(new_node_id, spatial_node);
+        self.id_to_index_mapper.map_clip_node(new_node_id, node_index);
         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,
         frame_rect: &LayoutRect,
         content_size: &LayoutSize,
         scroll_sensitivity: ScrollSensitivity,
     ) -> SpatialNodeIndex {
-        let node_index = self.get_spatial_node_index_for_clip_id(new_node_id);
-        let parent_node_index = self.get_spatial_node_index_for_clip_id(parent_id);
-        self.clip_scroll_tree.add_scroll_frame(
-            node_index,
+        let parent_node_index = self.id_to_index_mapper.get_spatial_node_index(parent_id);
+        let node_index = self.clip_scroll_tree.add_scroll_frame(
             parent_node_index,
             external_id,
             pipeline_id,
             frame_rect,
             content_size,
             scroll_sensitivity,
         );
+        self.id_to_index_mapper.map_spatial_node(new_node_id, node_index);
         self.id_to_index_mapper.map_to_parent_clip_chain(new_node_id, &parent_id);
         node_index
     }
 
     pub fn pop_reference_frame(&mut self) {
         self.reference_frame_stack.pop();
     }
 
@@ -1450,17 +1406,17 @@ impl<'a> DisplayListFlattener<'a> {
 
         let prim = BrushPrimitive::new(
             BrushKind::new_solid(color),
             None,
         );
 
         let prim_index = self.create_primitive(
             info,
-            Vec::new(),
+            None,
             PrimitiveContainer::Brush(prim),
         );
 
         self.add_primitive_to_draw_list(
             prim_index,
             clip_and_scroll,
         );
 
@@ -1962,38 +1918,24 @@ impl<'a> DisplayListFlattener<'a> {
             info,
             Vec::new(),
             PrimitiveContainer::Brush(prim),
         );
     }
 
     pub fn map_clip_and_scroll(&mut self, info: &ClipAndScrollInfo) -> ScrollNodeAndClipChain {
         ScrollNodeAndClipChain::new(
-            self.get_spatial_node_index_for_clip_id(info.scroll_node_id),
-            self.id_to_index_mapper.get_clip_chain_index_and_cache_result(&info.clip_node_id())
+            self.id_to_index_mapper.get_spatial_node_index(info.scroll_node_id),
+            self.id_to_index_mapper.get_clip_chain_index(&info.clip_node_id())
         )
     }
 
     pub fn simple_scroll_and_clip_chain(&mut self, id: &ClipId) -> ScrollNodeAndClipChain {
         self.map_clip_and_scroll(&ClipAndScrollInfo::simple(*id))
     }
-
-    pub fn get_spatial_node_index_for_clip_id(&mut self, id: ClipId,) -> SpatialNodeIndex {
-        match id {
-            ClipId::Spatial(index, pipeline_id) => {
-                let pipeline_offset = self.id_to_index_mapper.get_pipeline_offet(pipeline_id);
-                SpatialNodeIndex(pipeline_offset.spatial_offset + index)
-            }
-            ClipId::Clip(..) => {
-                let clip_node_index = self.id_to_index_mapper.get_clip_node_index(id);
-                self.clip_scroll_tree.clip_nodes[clip_node_index.0].spatial_node
-            }
-            ClipId::ClipChain(_) => panic!("Tried to use ClipChain as scroll node."),
-        }
-    }
 }
 
 pub fn build_scene(config: &FrameBuilderConfig, request: SceneRequest) -> BuiltScene {
 
     let mut clip_scroll_tree = ClipScrollTree::new();
     let mut new_scene = Scene::new();
 
     let frame_builder = DisplayListFlattener::create_frame_builder(
--- a/gfx/webrender/src/freelist.rs
+++ b/gfx/webrender/src/freelist.rs
@@ -1,14 +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 std::marker::PhantomData;
-use util::recycle_vec;
 
 // TODO(gw): Add an occupied list head, for fast
 //           iteration of the occupied list to implement
 //           retain() style functionality.
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -81,25 +80,16 @@ impl<T, M> FreeList<T, M> {
         FreeList {
             slots: Vec::new(),
             free_list_head: None,
             active_count: 0,
             _marker: PhantomData,
         }
     }
 
-    pub fn recycle(self) -> FreeList<T, M> {
-        FreeList {
-            slots: recycle_vec(self.slots),
-            free_list_head: None,
-            active_count: 0,
-            _marker: PhantomData,
-        }
-    }
-
     pub fn clear(&mut self) {
         self.slots.clear();
         self.free_list_head = None;
         self.active_count = 0;
     }
 
     #[allow(dead_code)]
     pub fn get(&self, id: &FreeListHandle<M>) -> &T {
--- a/gfx/webrender/src/gpu_types.rs
+++ b/gfx/webrender/src/gpu_types.rs
@@ -190,16 +190,17 @@ impl PrimitiveHeaders {
         });
 
         PrimitiveHeaderIndex(id as i32)
     }
 }
 
 // This is a convenience type used to make it easier to pass
 // the common parts around during batching.
+#[derive(Debug)]
 pub struct PrimitiveHeader {
     pub local_rect: LayoutRect,
     pub local_clip_rect: LayoutRect,
     pub task_address: RenderTaskAddress,
     pub specific_prim_address: GpuCacheAddress,
     pub clip_task_address: RenderTaskAddress,
     pub transform_id: TransformPaletteId,
 }
@@ -344,22 +345,25 @@ impl From<BrushInstance> for PrimitiveIn
 // pixel snapping applied).
 #[derive(Copy, Debug, Clone, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[repr(C)]
 pub struct TransformPaletteId(pub u32);
 
 impl TransformPaletteId {
-    // Get the palette ID for an identity transform.
-    pub fn identity() -> TransformPaletteId {
-        TransformPaletteId(0)
+    /// Identity transform ID.
+    pub const IDENTITY: Self = TransformPaletteId(0);
+
+    /// Extract the spatial node index from the id.
+    pub fn _spatial_node_index(&self) -> SpatialNodeIndex {
+        SpatialNodeIndex(self.0 as usize & 0xFFFFFF)
     }
 
-    // Extract the transform kind from the id.
+    /// Extract the transform kind from the id.
     pub fn transform_kind(&self) -> TransformedRectKind {
         if (self.0 >> 24) == 0 {
             TransformedRectKind::AxisAligned
         } else {
             TransformedRectKind::Complex
         }
     }
 }
--- a/gfx/webrender/src/hit_test.rs
+++ b/gfx/webrender/src/hit_test.rs
@@ -29,16 +29,39 @@ pub struct HitTestClipNode {
     /// The positioning node for this clip node.
     spatial_node: SpatialNodeIndex,
 
     /// A particular point must be inside all of these regions to be considered clipped in
     /// for the purposes of a hit test.
     regions: Vec<HitTestRegion>,
 }
 
+impl HitTestClipNode {
+    fn new(node: &ClipNode, clip_store: &ClipStore) -> Self {
+        let clips = clip_store.get(node.clip_sources_index);
+        let regions = clips.clips().iter().map(|source| {
+            match source.0 {
+                ClipSource::Rectangle(ref rect, mode) => HitTestRegion::Rectangle(*rect, mode),
+                ClipSource::RoundedRectangle(ref rect, ref radii, ref mode) =>
+                    HitTestRegion::RoundedRectangle(*rect, *radii, *mode),
+                ClipSource::Image(ref mask) => HitTestRegion::Rectangle(mask.rect, ClipMode::Clip),
+                ClipSource::LineDecoration(_) |
+                ClipSource::BoxShadow(_) => {
+                    unreachable!("Didn't expect to hit test against BorderCorner / BoxShadow / LineDecoration");
+                }
+            }
+        }).collect();
+
+        HitTestClipNode {
+            spatial_node: clips.spatial_node_index,
+            regions,
+        }
+    }
+}
+
 /// A description of a clip chain in the HitTester. This is used to describe
 /// hierarchical clip scroll nodes as well as ClipChains, so that they can be
 /// handled the same way during hit testing. Once we represent all ClipChains
 /// using ClipChainDescriptors, we can get rid of this and just use the
 /// ClipChainDescriptor here.
 #[derive(Clone)]
 struct HitTestClipChainDescriptor {
     parent: Option<ClipChainIndex>,
@@ -143,25 +166,21 @@ impl HitTester {
             self.spatial_nodes.push(HitTestSpatialNode {
                 pipeline_id: node.pipeline_id,
                 world_content_transform: node.world_content_transform,
                 world_viewport_transform: node.world_viewport_transform,
             });
         }
 
         for (index, node) in clip_scroll_tree.clip_nodes.iter().enumerate() {
-            self.clip_nodes.push(HitTestClipNode {
-                spatial_node: node.spatial_node,
-                regions: get_regions_for_clip_node(node, clip_store),
-            });
-
-             let clip_chain = self.clip_chains.get_mut(node.clip_chain_index.0).unwrap();
-             clip_chain.parent =
-                 clip_scroll_tree.get_clip_chain(node.clip_chain_index).parent_index;
-             clip_chain.clips = vec![ClipNodeIndex(index)];
+            self.clip_nodes.push(HitTestClipNode::new(node, clip_store));
+            let clip_chain = self.clip_chains.get_mut(node.clip_chain_index.0).unwrap();
+            clip_chain.parent =
+                clip_scroll_tree.get_clip_chain(node.clip_chain_index).parent_index;
+            clip_chain.clips = vec![ClipNodeIndex(index)];
         }
 
         for descriptor in &clip_scroll_tree.clip_chains_descriptors {
             let clip_chain = self.clip_chains.get_mut(descriptor.index.0).unwrap();
             clip_chain.parent = clip_scroll_tree.get_clip_chain(descriptor.index).parent_index;
             clip_chain.clips = descriptor.clips.clone();
         }
     }
@@ -341,43 +360,16 @@ impl HitTester {
         result
     }
 
     pub fn get_pipeline_root(&self, pipeline_id: PipelineId) -> &HitTestSpatialNode {
         &self.spatial_nodes[self.pipeline_root_nodes[&pipeline_id].0]
     }
 }
 
-fn get_regions_for_clip_node(
-    node: &ClipNode,
-    clip_store: &ClipStore
-) -> Vec<HitTestRegion> {
-    let handle = match node.handle.as_ref() {
-        Some(handle) => handle,
-        None => {
-            warn!("Encountered an empty clip node unexpectedly.");
-            return Vec::new()
-        }
-    };
-
-    let clips = clip_store.get(handle).clips();
-    clips.iter().map(|source| {
-        match source.0 {
-            ClipSource::Rectangle(ref rect, mode) => HitTestRegion::Rectangle(*rect, mode),
-            ClipSource::RoundedRectangle(ref rect, ref radii, ref mode) =>
-                HitTestRegion::RoundedRectangle(*rect, *radii, *mode),
-            ClipSource::Image(ref mask) => HitTestRegion::Rectangle(mask.rect, ClipMode::Clip),
-            ClipSource::LineDecoration(_) |
-            ClipSource::BoxShadow(_) => {
-                unreachable!("Didn't expect to hit test against BorderCorner / BoxShadow / LineDecoration");
-            }
-        }
-    }).collect()
-}
-
 #[derive(Clone, Copy, PartialEq)]
 enum ClippedIn {
     ClippedIn,
     NotClippedIn,
 }
 
 pub struct HitTest {
     pipeline_id: Option<PipelineId>,
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -8,17 +8,17 @@ use api::{FilterOp, GlyphInstance, Gradi
 use api::{GlyphRasterSpace, LayoutPoint, LayoutRect, LayoutSize, LayoutToWorldTransform, LayoutVector2D};
 use api::{PipelineId, PremultipliedColorF, PropertyBinding, Shadow, YuvColorSpace, YuvFormat, DeviceIntSideOffsets};
 use api::{BorderWidths, LayoutToWorldScale, NormalBorder};
 use app_units::Au;
 use border::{BorderCacheKey, BorderRenderTaskInfo};
 use box_shadow::BLUR_SAMPLE_SCALE;
 use clip_scroll_tree::{ClipChainIndex, CoordinateSystemId, SpatialNodeIndex};
 use clip::{ClipChain, ClipChainNode, ClipChainNodeIter, ClipChainNodeRef, ClipSource};
-use clip::{ClipSourcesHandle, ClipWorkItem};
+use clip::{ClipSourcesIndex, ClipWorkItem};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveRunContext;
 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, PictureId, PicturePrimitive};
@@ -183,17 +183,17 @@ pub struct ScreenRect {
     pub clipped: DeviceIntRect,
     pub unclipped: DeviceIntRect,
 }
 
 // TODO(gw): Pack the fields here better!
 #[derive(Debug)]
 pub struct PrimitiveMetadata {
     pub opacity: PrimitiveOpacity,
-    pub clip_sources: Option<ClipSourcesHandle>,
+    pub clip_sources_index: Option<ClipSourcesIndex>,
     pub prim_kind: PrimitiveKind,
     pub cpu_prim_index: SpecificPrimitiveIndex,
     pub gpu_location: GpuCacheHandle,
     pub clip_task_id: Option<RenderTaskId>,
 
     // TODO(gw): In the future, we should just pull these
     //           directly from the DL item, instead of
     //           storing them here.
@@ -1276,24 +1276,24 @@ impl PrimitiveStore {
         picture_index
     }
 
     pub fn add_primitive(
         &mut self,
         local_rect: &LayoutRect,
         local_clip_rect: &LayoutRect,
         is_backface_visible: bool,
-        clip_sources: Option<ClipSourcesHandle>,
+        clip_sources_index: Option<ClipSourcesIndex>,
         tag: Option<ItemTag>,
         container: PrimitiveContainer,
     ) -> PrimitiveIndex {
         let prim_index = self.cpu_metadata.len();
 
         let base_metadata = PrimitiveMetadata {
-            clip_sources,
+            clip_sources_index,
             gpu_location: GpuCacheHandle::new(),
             clip_task_id: None,
             local_rect: *local_rect,
             local_clip_rect: *local_clip_rect,
             combined_local_clip_rect: *local_clip_rect,
             is_backface_visible,
             screen_rect: None,
             tag,
@@ -1978,16 +1978,19 @@ impl PrimitiveStore {
                 }
                 PrimitiveKind::Brush => {
                     let brush = &self.cpu_brushes[metadata.cpu_prim_index.0];
                     brush.write_gpu_blocks(&mut request, metadata.local_rect);
 
                     match brush.segment_desc {
                         Some(ref segment_desc) => {
                             for segment in &segment_desc.segments {
+                                if cfg!(debug_assertions) && self.chase_id == Some(prim_index) {
+                                    println!("\t\t{:?}", segment);
+                                }
                                 // has to match VECS_PER_SEGMENT
                                 request.write_segment(
                                     segment.local_rect,
                                     segment.extra_data,
                                 );
                             }
                         }
                         None => {
@@ -2054,32 +2057,32 @@ impl PrimitiveStore {
         };
 
         // Segment the primitive on all the local-space clip sources that we can.
         for clip_item in clips {
             if clip_item.coordinate_system_id != prim_run_context.scroll_node.coordinate_system_id {
                 continue;
             }
 
-            let local_clips = frame_state.clip_store.get_opt(&clip_item.clip_sources).expect("bug");
+            let local_clips = frame_state.clip_store.get(clip_item.clip_sources_index);
             rect_clips_only = rect_clips_only && local_clips.only_rectangular_clips;
 
             // TODO(gw): We can easily extend the segment builder to support these clip sources in
             // the future, but they are rarely used.
             // We must do this check here in case we continue early below.
             if local_clips.has_image_or_line_decoration_clip {
                 clip_mask_kind = BrushClipMaskKind::Global;
             }
 
             // If this clip item is positioned by another positioning node, its relative position
             // could change during scrolling. This means that we would need to resegment. Instead
             // of doing that, only segment with clips that have the same positioning node.
             // TODO(mrobinson, #2858): It may make sense to include these nodes, resegmenting only
             // when necessary while scrolling.
-            if clip_item.spatial_node_index != prim_run_context.spatial_node_index {
+            if local_clips.spatial_node_index != prim_run_context.spatial_node_index {
                 // We don't need to generate a global clip mask for rectangle clips because we are
                 // in the same coordinate system and rectangular clips are handled by the local
                 // clip chain rectangle.
                 if !local_clips.only_rectangular_clips {
                     clip_mask_kind = BrushClipMaskKind::Global;
                 }
                 continue;
             }
@@ -2280,18 +2283,18 @@ impl PrimitiveStore {
             println!("\tbase screen {:?}, combined clip chain {:?}",
                 prim_screen_rect, prim_run_context.clip_chain.combined_outer_screen_rect);
         }
 
         let prim_coordinate_system_id = prim_run_context.scroll_node.coordinate_system_id;
         let transform = &prim_run_context.scroll_node.world_content_transform;
         let extra_clip =  {
             let metadata = &self.cpu_metadata[prim_index.0];
-            metadata.clip_sources.as_ref().map(|clip_sources| {
-                let prim_clips = frame_state.clip_store.get_mut(clip_sources);
+            metadata.clip_sources_index.map(|clip_sources_index| {
+                let prim_clips = frame_state.clip_store.get_mut(clip_sources_index);
                 prim_clips.update(
                     frame_state.gpu_cache,
                     frame_state.resource_cache,
                     frame_context.device_pixel_scale,
                 );
                 let (screen_inner_rect, screen_outer_rect) = prim_clips.get_screen_bounds(
                     transform,
                     frame_context.device_pixel_scale,
@@ -2302,18 +2305,17 @@ impl PrimitiveStore {
                     combined_outer_rect = combined_outer_rect.and_then(|r| r.intersection(&outer));
                 }
                 if cfg!(debug_assertions) && Some(prim_index) == self.chase_id {
                     println!("\tfound extra clip with screen bounds {:?}", screen_outer_rect);
                 }
 
                 Arc::new(ClipChainNode {
                     work_item: ClipWorkItem {
-                        spatial_node_index: prim_run_context.spatial_node_index,
-                        clip_sources: clip_sources.weak(),
+                        clip_sources_index,
                         coordinate_system_id: prim_coordinate_system_id,
                     },
                     // The local_clip_rect a property of ClipChain nodes that are ClipNodes.
                     // It's used to calculate a local clipping rectangle before we reach this
                     // point, so we can set it to zero here. It should be unused from this point
                     // on.
                     local_clip_rect: LayoutRect::zero(),
                     screen_inner_rect,
@@ -2638,29 +2640,32 @@ impl PrimitiveStore {
         frame_state: &mut FrameBuildingState,
     ) -> PrimitiveRunLocalRect {
         let mut result = PrimitiveRunLocalRect {
             local_rect_in_actual_parent_space: LayoutRect::zero(),
             local_rect_in_original_parent_space: LayoutRect::zero(),
         };
 
         for run in &pic_context.prim_runs {
-            if run.is_chasing(self.chase_id) {
-                println!("\tpreparing a run of length {} in pipeline {:?}",
-                    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_chains[run.clip_and_scroll.clip_chain_index.0];
 
+            if run.is_chasing(self.chase_id) {
+                println!("\tpreparing a run of length {} in pipeline {:?}",
+                    run.count, pic_context.pipeline_id);
+                println!("\trun {:?}", run.clip_and_scroll);
+                println!("\ttransform {:?}", scroll_node.world_content_transform.to_transform());
+            }
+
             // 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 {
                 if run.is_chasing(self.chase_id) {
@@ -2711,17 +2716,22 @@ impl PrimitiveStore {
 
             let clip_chain_rect = if pic_context.apply_local_clip_rect {
                 get_local_clip_rect_for_nodes(scroll_node, clip_chain)
             } else {
                 None
             };
 
             let local_clip_chain_rect = match clip_chain_rect {
-                Some(rect) if rect.is_empty() => continue,
+                Some(rect) if rect.is_empty() => {
+                    if run.is_chasing(self.chase_id) {
+                        println!("\tculled by empty chain rect");
+                    }
+                    continue
+                },
                 Some(rect) => rect,
                 None => frame_context.max_local_clip,
             };
 
             let transform = frame_context
                 .transforms
                 .get_transform(run.clip_and_scroll.spatial_node_index);
 
@@ -2744,17 +2754,22 @@ impl PrimitiveStore {
                     frame_context,
                     frame_state,
                 ) {
                     frame_state.profile_counters.visible_primitives.inc();
 
                     let clipped_rect = match clip_chain_rect {
                         Some(ref chain_rect) => match prim_local_rect.intersection(chain_rect) {
                             Some(rect) => rect,
-                            None => continue,
+                            None => {
+                                if cfg!(debug_assertions) && self.chase_id == Some(prim_index) {
+                                    println!("\tculled by chain rect {:?}", chain_rect);
+                                }
+                                continue
+                            },
                         },
                         None => prim_local_rect,
                     };
 
                     if let Some(ref matrix) = parent_relative_transform {
                         match matrix.transform_rect(&clipped_rect) {
                             Some(bounds) => {
                                 result.local_rect_in_actual_parent_space =
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -164,16 +164,107 @@ impl Document {
     }
 
     fn can_render(&self) -> bool { self.frame_builder.is_some() }
 
     fn has_pixels(&self) -> bool {
         !self.view.window_size.is_empty_or_negative()
     }
 
+    fn process_frame_msg(
+        &mut self,
+        message: FrameMsg,
+    ) -> DocumentOps {
+        match message {
+            FrameMsg::UpdateEpoch(pipeline_id, epoch) => {
+                self.current.scene.update_epoch(pipeline_id, epoch);
+
+                DocumentOps::nop()
+            }
+            FrameMsg::EnableFrameOutput(pipeline_id, enable) => {
+                if enable {
+                    self.output_pipelines.insert(pipeline_id);
+                } else {
+                    self.output_pipelines.remove(&pipeline_id);
+                }
+                DocumentOps::nop()
+            }
+            FrameMsg::Scroll(delta, cursor) => {
+                profile_scope!("Scroll");
+
+                let mut should_render = true;
+                let node_index = match self.hit_tester {
+                    Some(ref hit_tester) => {
+                        // Ideally we would call self.scroll_nearest_scrolling_ancestor here, but
+                        // we need have to avoid a double-borrow.
+                        let test = HitTest::new(None, cursor, HitTestFlags::empty());
+                        hit_tester.find_node_under_point(test)
+                    }
+                    None => {
+                        should_render = false;
+                        None
+                    }
+                };
+
+                let should_render =
+                    should_render &&
+                    self.scroll_nearest_scrolling_ancestor(delta, node_index) &&
+                    self.render_on_scroll == Some(true);
+                DocumentOps {
+                    scroll: true,
+                    render: should_render,
+                    composite: should_render,
+                    ..DocumentOps::nop()
+                }
+            }
+            FrameMsg::HitTest(pipeline_id, point, flags, tx) => {
+
+                let result = match self.hit_tester {
+                    Some(ref hit_tester) => {
+                        hit_tester.hit_test(HitTest::new(pipeline_id, point, flags))
+                    }
+                    None => HitTestResult { items: Vec::new() },
+                };
+
+                tx.send(result).unwrap();
+                DocumentOps::nop()
+            }
+            FrameMsg::SetPan(pan) => {
+                self.view.pan = pan;
+                DocumentOps::nop()
+            }
+            FrameMsg::ScrollNodeWithId(origin, id, clamp) => {
+                profile_scope!("ScrollNodeWithScrollId");
+
+                let should_render = self.scroll_node(origin, id, clamp)
+                    && self.render_on_scroll == Some(true);
+
+                DocumentOps {
+                    scroll: true,
+                    render: should_render,
+                    composite: should_render,
+                    ..DocumentOps::nop()
+                }
+            }
+            FrameMsg::GetScrollNodeState(tx) => {
+                profile_scope!("GetScrollNodeState");
+                tx.send(self.get_scroll_node_state()).unwrap();
+                DocumentOps::nop()
+            }
+            FrameMsg::UpdateDynamicProperties(property_bindings) => {
+                self.dynamic_properties.set_properties(property_bindings);
+                DocumentOps::nop()
+            }
+            FrameMsg::AppendDynamicProperties(property_bindings) => {
+                self.dynamic_properties.add_properties(property_bindings);
+                DocumentOps::nop()
+            }
+        }
+    }
+
     // TODO: We will probably get rid of this soon and always forward to the scene building thread.
     fn build_scene(&mut self, resource_cache: &mut ResourceCache, scene_id: u64) {
         let max_texture_size = resource_cache.max_texture_size();
 
         if self.view.window_size.width > max_texture_size ||
            self.view.window_size.height > max_texture_size {
             error!("ERROR: Invalid window dimensions {}x{}. Please call api.set_window_size()",
                 self.view.window_size.width,
@@ -596,110 +687,16 @@ impl RenderBackend {
 
                 doc.pending.scene.remove_pipeline(pipeline_id);
                 doc.pending.removed_pipelines.push(pipeline_id);
                 DocumentOps::nop()
             }
         }
     }
 
-    fn process_frame_msg(
-        &mut self,
-        document_id: DocumentId,
-        message: FrameMsg,
-    ) -> DocumentOps {
-        let doc = self.documents.get_mut(&document_id).expect("No document?");
-
-        match message {
-            FrameMsg::UpdateEpoch(pipeline_id, epoch) => {
-                doc.current.scene.update_epoch(pipeline_id, epoch);
-
-                DocumentOps::nop()
-            }
-            FrameMsg::EnableFrameOutput(pipeline_id, enable) => {
-                if enable {
-                    doc.output_pipelines.insert(pipeline_id);
-                } else {
-                    doc.output_pipelines.remove(&pipeline_id);
-                }
-                DocumentOps::nop()
-            }
-            FrameMsg::Scroll(delta, cursor) => {
-                profile_scope!("Scroll");
-
-                let mut should_render = true;
-                let node_index = match doc.hit_tester {
-                    Some(ref hit_tester) => {
-                        // Ideally we would call doc.scroll_nearest_scrolling_ancestor here, but
-                        // we need have to avoid a double-borrow.
-                        let test = HitTest::new(None, cursor, HitTestFlags::empty());
-                        hit_tester.find_node_under_point(test)
-                    }
-                    None => {
-                        should_render = false;
-                        None
-                    }
-                };
-
-                let should_render =
-                    should_render &&
-                    doc.scroll_nearest_scrolling_ancestor(delta, node_index) &&
-                    doc.render_on_scroll == Some(true);
-                DocumentOps {
-                    scroll: true,
-                    render: should_render,
-                    composite: should_render,
-                    ..DocumentOps::nop()
-                }
-            }
-            FrameMsg::HitTest(pipeline_id, point, flags, tx) => {
-
-                let result = match doc.hit_tester {
-                    Some(ref hit_tester) => {
-                        hit_tester.hit_test(HitTest::new(pipeline_id, point, flags))
-                    }
-                    None => HitTestResult { items: Vec::new() },
-                };
-
-                tx.send(result).unwrap();
-                DocumentOps::nop()
-            }
-            FrameMsg::SetPan(pan) => {
-                doc.view.pan = pan;
-                DocumentOps::nop()
-            }
-            FrameMsg::ScrollNodeWithId(origin, id, clamp) => {
-                profile_scope!("ScrollNodeWithScrollId");
-
-                let should_render = doc.scroll_node(origin, id, clamp)
-                    && doc.render_on_scroll == Some(true);
-
-                DocumentOps {
-                    scroll: true,
-                    render: should_render,
-                    composite: should_render,
-                    ..DocumentOps::nop()
-                }
-            }
-            FrameMsg::GetScrollNodeState(tx) => {
-                profile_scope!("GetScrollNodeState");
-                tx.send(doc.get_scroll_node_state()).unwrap();
-                DocumentOps::nop()
-            }
-            FrameMsg::UpdateDynamicProperties(property_bindings) => {
-                doc.dynamic_properties.set_properties(property_bindings);
-                DocumentOps::render()
-            }
-            FrameMsg::AppendDynamicProperties(property_bindings) => {
-                doc.dynamic_properties.add_properties(property_bindings);
-                DocumentOps::render()
-            }
-        }
-    }
-
     fn next_namespace_id(&self) -> IdNamespace {
         IdNamespace(NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed) as u32)
     }
 
     pub fn make_unique_scene_id(&mut self) -> u64 {
         // 2^64 scenes ought to be enough for anybody!
         self.last_scene_id += 1;
         self.last_scene_id
@@ -1065,22 +1062,26 @@ impl RenderBackend {
         // before rendering. This is useful for rendering with the latest
         // async transforms.
         if op.render || transaction_msg.generate_frame {
             if let Some(ref sampler) = self.sampler {
                 transaction_msg.frame_ops.append(&mut sampler.sample());
             }
         }
 
+        let doc = self.documents.get_mut(&document_id).unwrap();
+
         for frame_msg in transaction_msg.frame_ops {
             let _timer = profile_counters.total_time.timer();
-            op.combine(self.process_frame_msg(document_id, frame_msg));
+            op.combine(doc.process_frame_msg(frame_msg));
         }
 
-        let doc = self.documents.get_mut(&document_id).unwrap();
+        if doc.dynamic_properties.flush_pending_updates() {
+            op.render = true;
+        }
 
         if transaction_msg.generate_frame {
             if let Some(ref mut ros) = doc.render_on_scroll {
                 *ros = true;
             }
 
             if doc.current.scene.root_pipeline_id.is_some() {
                 op.render = true;
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -398,17 +398,17 @@ impl RenderTask {
         // task cache. This allows the blurred box-shadow rect to be cached
         // in the texture cache across frames.
         // TODO(gw): Consider moving this logic outside this function, especially
         //           as we add more clip sources that depend on render tasks.
         // TODO(gw): If this ever shows up in a profile, we could pre-calculate
         //           whether a ClipSources contains any box-shadows and skip
         //           this iteration for the majority of cases.
         for clip_item in &clips {
-            let clip_sources = clip_store.get_opt_mut(&clip_item.clip_sources).expect("bug");
+            let clip_sources = clip_store.get_mut(clip_item.clip_sources_index);
             for &mut (ref mut clip, _) in &mut clip_sources.clips {
                 match *clip {
                     ClipSource::BoxShadow(ref mut info) => {
                         let (cache_size, cache_key) = info.cache_key
                             .as_ref()
                             .expect("bug: no cache key set")
                             .clone();
                         let blur_radius_dp = cache_key.blur_radius_dp as f32;
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -1230,45 +1230,41 @@ impl ResourceCache {
         }
     }
 
     pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
         self.glyph_rasterizer.get_glyph_index(font_key, ch)
     }
 
     #[inline]
-    pub fn get_cached_image(
-        &self,
-        request: ImageRequest,
-    ) -> Result<CacheItem, ()> {
+    pub fn get_cached_image(&self, request: ImageRequest) -> Result<CacheItem, ()> {
         debug_assert_eq!(self.state, State::QueryResources);
-
-        // TODO(Jerry): add a debug option to visualize the corresponding area for
-        // the Err() case of CacheItem.
-        match *self.cached_images.get(&request.key) {
-            ImageResult::UntiledAuto(ref image_info) => {
-                Ok(self.texture_cache.get(&image_info.texture_cache_handle))
-            }
-            ImageResult::Multi(ref entries) => {
-                let image_info = entries.get(&request.into());
-                Ok(self.texture_cache.get(&image_info.texture_cache_handle))
-            }
-            ImageResult::Err(_) => {
-                Err(())
-            }
-        }
+        let image_info = self.get_image_info(request)?;
+        Ok(self.get_texture_cache_item(&image_info.texture_cache_handle))
     }
 
     pub fn get_cached_render_task(
         &self,
         handle: &RenderTaskCacheEntryHandle,
     ) -> &RenderTaskCacheEntry {
         self.cached_render_tasks.get_cache_entry(handle)
     }
 
+    #[inline]
+    fn get_image_info(&self, request: ImageRequest) -> Result<&CachedImageInfo, ()> {
+        // TODO(Jerry): add a debug option to visualize the corresponding area for
+        // the Err() case of CacheItem.
+        match *self.cached_images.get(&request.key) {
+            ImageResult::UntiledAuto(ref image_info) => Ok(image_info),
+            ImageResult::Multi(ref entries) => Ok(entries.get(&request.into())),
+            ImageResult::Err(_) => Err(()),
+        }
+    }
+
+    #[inline]
     pub fn get_texture_cache_item(&self, handle: &TextureCacheHandle) -> CacheItem {
         self.texture_cache.get(handle)
     }
 
     pub fn get_image_properties(&self, image_key: ImageKey) -> Option<ImageProperties> {
         let image_template = &self.resources.image_templates.get(image_key);
 
         image_template.map(|image_template| {
--- a/gfx/webrender/src/scene.rs
+++ b/gfx/webrender/src/scene.rs
@@ -8,48 +8,82 @@ use api::{ItemRange, MixBlendMode, Stack
 use internal_types::FastHashMap;
 use std::sync::Arc;
 
 /// Stores a map of the animated property bindings for the current display list. These
 /// can be used to animate the transform and/or opacity of a display list without
 /// re-submitting the display list itself.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
-#[derive(Clone)]
 pub struct SceneProperties {
     transform_properties: FastHashMap<PropertyBindingId, LayoutTransform>,
     float_properties: FastHashMap<PropertyBindingId, f32>,
+    current_properties: DynamicProperties,
+    pending_properties: Option<DynamicProperties>,
 }
 
 impl SceneProperties {
     pub fn new() -> Self {
         SceneProperties {
             transform_properties: FastHashMap::default(),
             float_properties: FastHashMap::default(),
+            current_properties: DynamicProperties::default(),
+            pending_properties: None,
         }
     }
 
     /// Set the current property list for this display list.
     pub fn set_properties(&mut self, properties: DynamicProperties) {
-        self.transform_properties.clear();
-        self.float_properties.clear();
-        self.add_properties(properties);
+        self.pending_properties = Some(properties);
     }
 
     /// Add to the current property list for this display list.
     pub fn add_properties(&mut self, properties: DynamicProperties) {
-        for property in properties.transforms {
-            self.transform_properties
-                .insert(property.key.id, property.value);
+        let mut pending_properties = self.pending_properties
+            .take()
+            .unwrap_or_default();
+
+        pending_properties.transforms.extend(properties.transforms);
+        pending_properties.floats.extend(properties.floats);
+
+        self.pending_properties = Some(pending_properties);
+    }
+
+    /// Flush any pending updates to the scene properties. Returns
+    /// true if the properties have changed since the last flush
+    /// was called. This code allows properties to be changed by
+    /// multiple set_properties and add_properties calls during a
+    /// single transaction, and still correctly determine if any
+    /// properties have changed. This can have significant power
+    /// saving implications, allowing a frame build to be skipped
+    /// if the properties haven't changed in many cases.
+    pub fn flush_pending_updates(&mut self) -> bool {
+        let mut properties_changed = false;
+
+        if let Some(pending_properties) = self.pending_properties.take() {
+            if pending_properties != self.current_properties {
+                self.transform_properties.clear();
+                self.float_properties.clear();
+
+                for property in &pending_properties.transforms {
+                    self.transform_properties
+                        .insert(property.key.id, property.value);
+                }
+
+                for property in &pending_properties.floats {
+                    self.float_properties
+                        .insert(property.key.id, property.value);
+                }
+
+                self.current_properties = pending_properties;
+                properties_changed = true;
+            }
         }
 
-        for property in properties.floats {
-            self.float_properties
-                .insert(property.key.id, property.value);
-        }
+        properties_changed
     }
 
     /// Get the current value for a transform property.
     pub fn resolve_layout_transform(
         &self,
         property: &PropertyBinding<LayoutTransform>,
     ) -> LayoutTransform {
         match *property {
--- a/gfx/webrender/src/spatial_node.rs
+++ b/gfx/webrender/src/spatial_node.rs
@@ -21,21 +21,16 @@ pub enum SpatialNodeType {
     StickyFrame(StickyFrameInfo),
 
     /// Transforms it's content, but doesn't clip it. Can also be adjusted
     /// by scroll events or setting scroll offsets.
     ScrollFrame(ScrollFrameInfo),
 
     /// A reference frame establishes a new coordinate space in the tree.
     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,
 }
 
 /// 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
@@ -89,20 +84,16 @@ impl SpatialNode {
             pipeline_id,
             node_type,
             invertible: true,
             coordinate_system_id: CoordinateSystemId(0),
             coordinate_system_relative_transform: LayoutFastTransform::identity(),
         }
     }
 
-    pub fn empty() -> SpatialNode {
-        Self::new(PipelineId::dummy(), None, SpatialNodeType::Empty)
-    }
-
     pub fn new_scroll_frame(
         pipeline_id: PipelineId,
         parent_index: SpatialNodeIndex,
         external_id: Option<ExternalScrollId>,
         frame_rect: &LayoutRect,
         content_size: &LayoutSize,
         scroll_sensitivity: ScrollSensitivity,
     ) -> Self {
@@ -489,17 +480,16 @@ impl SpatialNode {
                 state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
                 state.coordinate_system_relative_transform =
                     self.coordinate_system_relative_transform.clone();
                 let translation = -info.origin_in_parent_reference_frame;
                 state.nearest_scrolling_ancestor_viewport =
                     state.nearest_scrolling_ancestor_viewport
                        .translate(&translation);
             }
-            SpatialNodeType::Empty => unreachable!("Empty node remaining in ClipScrollTree."),
         }
     }
 
     pub fn scrollable_size(&self) -> LayoutSize {
         match self.node_type {
            SpatialNodeType::ScrollFrame(state) => state.scrollable_size,
             _ => LayoutSize::zero(),
         }
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -1085,26 +1085,26 @@ pub enum PropertyBinding<T> {
 impl<T> From<T> for PropertyBinding<T> {
     fn from(value: T) -> PropertyBinding<T> {
         PropertyBinding::Value(value)
     }
 }
 
 /// The current value of an animated property. This is
 /// supplied by the calling code.
-#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
 pub struct PropertyValue<T> {
     pub key: PropertyBindingKey<T>,
     pub value: T,
 }
 
 /// When using `generate_frame()`, a list of `PropertyValue` structures
 /// can optionally be supplied to provide the current value of any
 /// animated properties.
-#[derive(Clone, Deserialize, Serialize, Debug)]
+#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
 pub struct DynamicProperties {
     pub transforms: Vec<PropertyValue<LayoutTransform>>,
     pub floats: Vec<PropertyValue<f32>>,
 }
 
 pub trait RenderNotifier: Send {
     fn clone(&self) -> Box<RenderNotifier>;
     fn wake_up(&self);
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-e850fbd2e0e60a8de76c2d2464f0fa27316d5949
+8a4fe66528aa362721e4048aac3cd5abf7faaf2c
--- a/gfx/wrench/src/main.rs
+++ b/gfx/wrench/src/main.rs
@@ -177,33 +177,28 @@ impl WindowWrapper {
         match *self {
             WindowWrapper::Window(ref window, _) => window.swap_buffers().unwrap(),
             WindowWrapper::Angle(_, ref context, _) => context.swap_buffers().unwrap(),
             WindowWrapper::Headless(_, _) => {}
         }
     }
 
     fn get_inner_size(&self) -> DeviceUintSize {
-        //HACK: `winit` needs to figure out its hidpi story...
-        #[cfg(target_os = "macos")]
-        fn inner_size(window: &winit::Window) -> LogicalSize {
-            let LogicalSize { width, height } = window.get_inner_size().unwrap();
-            let factor = window.get_hidpi_factor();
-            LogicalSize::new(width * factor, height * factor)
+        fn inner_size(window: &winit::Window) -> DeviceUintSize {
+            let size = window
+                .get_inner_size()
+                .unwrap()
+                .to_physical(window.get_hidpi_factor());
+            DeviceUintSize::new(size.width as u32, size.height as u32)
         }
-        #[cfg(not(target_os = "macos"))]
-        fn inner_size(window: &winit::Window) -> LogicalSize {
-            window.get_inner_size().unwrap()
-        }
-        let LogicalSize { width, height } = match *self {
+        match *self {
             WindowWrapper::Window(ref window, _) => inner_size(window.window()),
             WindowWrapper::Angle(ref window, ..) => inner_size(window),
-            WindowWrapper::Headless(ref context, _) => LogicalSize::new(context.width as f64, context.height as f64),
-        };
-        DeviceUintSize::new(width as u32, height as u32)
+            WindowWrapper::Headless(ref context, _) => DeviceUintSize::new(context.width, context.height),
+        }
     }
 
     fn hidpi_factor(&self) -> f32 {
         match *self {
             WindowWrapper::Window(ref window, _) => window.get_hidpi_factor() as f32,
             WindowWrapper::Angle(ref window, ..) => window.get_hidpi_factor() as f32,
             WindowWrapper::Headless(_, _) => 1.0,
         }