Bug 1477970 - Update webrender to commit 8a4fe66528aa362721e4048aac3cd5abf7faaf2c. r?jrmuizel draft
authorKartikaya Gupta <kgupta@mozilla.com>
Mon, 30 Jul 2018 09:35:05 -0400
changeset 824272 4314765ebd065582db173683abcf502ee8611481
parent 824238 dead9fcddd4a25fd36d54ab7eb782d7d9b8bb7a1
push id117852
push userkgupta@mozilla.com
push dateMon, 30 Jul 2018 13:49:22 +0000
reviewersjrmuizel
bugs1477970
milestone63.0a1
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,
         }