Bug 1531170 - Fix WR hit testing breakage caused by stacking context clip changes. r=kvark
authorGlenn Watson <github@intuitionlibrary.com>
Mon, 11 Mar 2019 16:19:21 +0000
changeset 521420 96e78962a05388c638385c2bad0cc772b7295291
parent 521419 4f506f3861997bfcea12588a0a724c54b5807ec6
child 521421 7b85bf9c5210e5679fa6cfad92466a6e2ba30232
push id10866
push usernerli@mozilla.com
push dateTue, 12 Mar 2019 18:59:09 +0000
treeherdermozilla-beta@445c24a51727 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskvark
bugs1531170
milestone67.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 1531170 - Fix WR hit testing breakage caused by stacking context clip changes. r=kvark Recently, semantics for clips on stacking contexts were changed such that primitives inherit the clip chain from all enclosing stacking contexts. However, the hit testing code was not updated to handle this change. As each hit testing primitive is added, the current stack of active stacking contexts is now scanned. Any valid clip chain roots from the primitive and/or the stacking context stack are added to the list of clip chain roots for this hit testing primitive. This patch also applies some optimizations and other cleanups of the hit-testing code, specifically: - Instead of cloning the hit testing runs Vec every time a frame is built, store these in the new HitTestingScene. The HitTestingScene is built once per scene, and then shared by any hit tester instances via an Arc. This reduces a lot of memory allocations and copying during scrolling. - When creating a new HitTestingScene, pre-allocate the size of the arrays, based on the size of the previous hit testing structure. This works similarly to how arrays are sized in the PrimitiveStore. - Pre-calculate and cache a number of inverse transform matrices that were previously being calculated for each hit testing run. - Store hit testing primitives in a flat array, instead of runs, since there is no longer a single clip chain id per primitive. - Fix an apparent (?) bug in the existing hit testing code, where clipping out a single hit test primitive would break out of the loop for the current run of hit test items. Differential Revision: https://phabricator.services.mozilla.com/D22635
gfx/wr/webrender/src/clip_scroll_tree.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/hit_test.rs
gfx/wr/webrender/src/scene_builder.rs
gfx/wr/webrender/src/util.rs
--- a/gfx/wr/webrender/src/clip_scroll_tree.rs
+++ b/gfx/wr/webrender/src/clip_scroll_tree.rs
@@ -5,17 +5,17 @@
 use api::{ExternalScrollId, LayoutPoint, LayoutRect, LayoutVector2D, ReferenceFrameKind};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::{LayoutSize, LayoutTransform, PropertyBinding, TransformStyle, WorldPoint};
 use gpu_types::TransformPalette;
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use scene::SceneProperties;
 use spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
-use std::ops;
+use std::{ops, u32};
 use util::{project_rect, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
 
 pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
 
 /// An id that identifies coordinate systems in the ClipScrollTree. Each
 /// coordinate system has an id and those ids will be shared when the coordinates
 /// system are the same or are in the same axis-aligned space. This allows
 /// for optimizing mask generation.
@@ -45,16 +45,20 @@ impl CoordinateSystem {
     }
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct SpatialNodeIndex(pub u32);
 
+impl SpatialNodeIndex {
+    pub const INVALID: SpatialNodeIndex = SpatialNodeIndex(u32::MAX);
+}
+
 //Note: these have to match ROOT_REFERENCE_FRAME_SPATIAL_ID and ROOT_SCROLL_NODE_SPATIAL_ID
 pub const ROOT_SPATIAL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(0);
 const TOPMOST_SCROLL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(1);
 
 impl SpatialNodeIndex {
     pub fn new(index: usize) -> Self {
         debug_assert!(index < ::std::u32::MAX as usize);
         SpatialNodeIndex(index as u32)
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -12,39 +12,39 @@ use api::{PropertyBinding, ReferenceFram
 use api::{Shadow, SpaceAndClipInfo, SpatialId, SpecificDisplayItem, StackingContext, StickyFrameDisplayItem};
 use api::{ClipMode, PrimitiveKeyKind, TransformStyle, YuvColorSpace, YuvData, TempFilterData};
 use api::units::*;
 use app_units::Au;
 use clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex};
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
-use hit_test::{HitTestingItem, HitTestingRun};
+use hit_test::{HitTestingItem, HitTestingScene};
 use image::simplify_repeated_primitive;
 use intern::Interner;
 use internal_types::{FastHashMap, FastHashSet};
 use picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
 use picture::{BlitReason, OrderedPictureChild, PrimitiveList, TileCache};
 use prim_store::{PrimitiveInstance, PrimitiveSceneData};
 use prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
-use prim_store::{PrimitiveStoreStats, ScrollNodeAndClipChain, PictureIndex};
+use prim_store::{ScrollNodeAndClipChain, PictureIndex};
 use prim_store::InternablePrimitive;
 use prim_store::{register_prim_chase_id, get_line_decoration_sizes};
 use prim_store::borders::{ImageBorder, NormalBorderPrim};
 use prim_store::gradient::{GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams};
 use prim_store::image::{Image, YuvImage};
 use prim_store::line_dec::{LineDecoration, LineDecorationCacheKey};
 use prim_store::picture::{Picture, PictureCompositeKey, PictureKey};
 use prim_store::text_run::TextRun;
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, StackingContextHelpers};
-use scene_builder::Interners;
+use scene_builder::{DocumentStats, Interners};
 use spatial_node::{StickyFrameInfo, ScrollFrameKind, SpatialNodeType};
-use std::{f32, mem, usize};
+use std::{f32, mem, usize, ops};
 use std::collections::vec_deque::VecDeque;
 use std::sync::Arc;
 use tiling::{CompositeOps};
 use util::{MaxRect, VecHelper};
 use ::filterdata::{SFilterDataComponent, SFilterData, SFilterDataKey};
 
 #[derive(Debug, Copy, Clone)]
 struct ClipNode {
@@ -200,17 +200,17 @@ pub struct DisplayListFlattener<'a> {
 
     /// The stack keeping track of the root clip chains associated with pipelines.
     pipeline_clip_chain_stack: Vec<ClipChainId>,
 
     /// The store of primitives.
     pub prim_store: PrimitiveStore,
 
     /// Information about all primitives involved in hit testing.
-    pub hit_testing_runs: Vec<HitTestingRun>,
+    pub hit_testing_scene: HitTestingScene,
 
     /// The store which holds all complex clipping information.
     pub clip_store: ClipStore,
 
     /// The configuration to use for the FrameBuilder. We consult this in
     /// order to determine the default font.
     pub config: FrameBuilderConfig,
 
@@ -230,38 +230,38 @@ impl<'a> DisplayListFlattener<'a> {
         scene: &Scene,
         clip_scroll_tree: &mut ClipScrollTree,
         font_instances: FontInstanceMap,
         view: &DocumentView,
         output_pipelines: &FastHashSet<PipelineId>,
         frame_builder_config: &FrameBuilderConfig,
         new_scene: &mut Scene,
         interners: &mut Interners,
-        prim_store_stats: &PrimitiveStoreStats,
+        doc_stats: &DocumentStats,
     ) -> FrameBuilder {
         // We checked that the root pipeline is available on the render backend.
         let root_pipeline_id = scene.root_pipeline_id.unwrap();
         let root_pipeline = scene.pipelines.get(&root_pipeline_id).unwrap();
 
         let background_color = root_pipeline
             .background_color
             .and_then(|color| if color.a > 0.0 { Some(color) } else { None });
 
         let mut flattener = DisplayListFlattener {
             scene,
             clip_scroll_tree,
             font_instances,
             config: *frame_builder_config,
             output_pipelines,
             id_to_index_mapper: NodeIdToIndexMapper::default(),
-            hit_testing_runs: Vec::new(),
+            hit_testing_scene: HitTestingScene::new(&doc_stats.hit_test_stats),
             pending_shadow_items: VecDeque::new(),
             sc_stack: Vec::new(),
             pipeline_clip_chain_stack: vec![ClipChainId::NONE],
-            prim_store: PrimitiveStore::new(&prim_store_stats),
+            prim_store: PrimitiveStore::new(&doc_stats.prim_store_stats),
             clip_store: ClipStore::new(),
             interners,
             root_pic_index: PictureIndex(0),
             rf_mapper: ReferenceFrameMapper::new(),
         };
 
         flattener.push_root(
             root_pipeline_id,
@@ -1187,27 +1187,44 @@ impl<'a> DisplayListFlattener<'a> {
         info: &LayoutPrimitiveInfo,
         clip_and_scroll: ScrollNodeAndClipChain
     ) {
         let tag = match info.tag {
             Some(tag) => tag,
             None => return,
         };
 
-        let new_item = HitTestingItem::new(tag, info);
-        match self.hit_testing_runs.last_mut() {
-            Some(&mut HitTestingRun(ref mut items, prev_clip_and_scroll))
-                if prev_clip_and_scroll == clip_and_scroll => {
-                items.push(new_item);
-                return;
-            }
-            _ => {}
+        // We want to get a range of clip chain roots that apply to this
+        // hit testing primitive.
+
+        // Get the start index for the clip chain root range for this primitive.
+        let start = self.hit_testing_scene.next_clip_chain_index();
+
+        // Add the clip chain root for the primitive itself.
+        self.hit_testing_scene.add_clip_chain(clip_and_scroll.clip_chain_id);
+
+        // Append any clip chain roots from enclosing stacking contexts.
+        for sc in &self.sc_stack {
+            self.hit_testing_scene.add_clip_chain(sc.clip_chain_id);
         }
 
-        self.hit_testing_runs.push(HitTestingRun(vec![new_item], clip_and_scroll));
+        // Construct a clip chain roots range to be stored with the item.
+        let clip_chain_range = ops::Range {
+            start,
+            end: self.hit_testing_scene.next_clip_chain_index(),
+        };
+
+        // Create and store the hit testing primitive itself.
+        let new_item = HitTestingItem::new(
+            tag,
+            info,
+            clip_and_scroll.spatial_node_index,
+            clip_chain_range,
+        );
+        self.hit_testing_scene.add_item(new_item);
     }
 
     /// Add an already created primitive to the draw lists.
     pub fn add_primitive_to_draw_list(
         &mut self,
         prim_instance: PrimitiveInstance,
     ) {
         // Add primitive to the top-most stacking context on the stack.
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -5,28 +5,31 @@
 use api::{ColorF, DebugFlags, DocumentLayer, FontRenderMode, PremultipliedColorF};
 use api::{PipelineId, RasterSpace};
 use api::units::*;
 use clip::{ClipDataStore, ClipStore, ClipChainStack};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::{GpuCache, GpuCacheHandle};
 use gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
-use hit_test::{HitTester, HitTestingRun};
+use hit_test::{HitTester, HitTestingScene};
+#[cfg(feature = "replay")]
+use hit_test::HitTestingSceneStats;
 use internal_types::{FastHashMap, PlaneSplitter};
 use picture::{PictureSurface, PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex};
 use picture::{RetainedTiles, TileCache, DirtyRegion};
 use prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
 #[cfg(feature = "replay")]
 use prim_store::{PrimitiveStoreStats};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use render_backend::{DataStores, FrameStamp};
 use render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskTree, RenderTaskTreeCounters};
 use resource_cache::{ResourceCache};
 use scene::{ScenePipeline, SceneProperties};
+use scene_builder::DocumentStats;
 use segment::SegmentBuilder;
 use spatial_node::SpatialNode;
 use std::{f32, mem};
 use std::sync::Arc;
 use tiling::{Frame, RenderPass, RenderPassKind, RenderTargetContext, RenderTarget};
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -99,17 +102,17 @@ pub struct FrameBuilder {
     background_color: Option<ColorF>,
     root_pic_index: PictureIndex,
     /// Cache of surface tiles from the previous frame builder
     /// that can optionally be consumed by this frame builder.
     pending_retained_tiles: RetainedTiles,
     pub prim_store: PrimitiveStore,
     pub clip_store: ClipStore,
     #[cfg_attr(feature = "capture", serde(skip))] //TODO
-    pub hit_testing_runs: Vec<HitTestingRun>,
+    pub hit_testing_scene: Arc<HitTestingScene>,
     pub config: FrameBuilderConfig,
     pub globals: FrameGlobalResources,
 }
 
 pub struct FrameVisibilityContext<'a> {
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub screen_world_rect: WorldRect,
     pub global_device_pixel_scale: DevicePixelScale,
@@ -214,17 +217,17 @@ impl<'a> PrimitiveContext<'a> {
         }
     }
 }
 
 impl FrameBuilder {
     #[cfg(feature = "replay")]
     pub fn empty() -> Self {
         FrameBuilder {
-            hit_testing_runs: Vec::new(),
+            hit_testing_scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
             prim_store: PrimitiveStore::new(&PrimitiveStoreStats::empty()),
             clip_store: ClipStore::new(),
             output_rect: DeviceIntRect::zero(),
             background_color: None,
             root_pic_index: PictureIndex(0),
             pending_retained_tiles: RetainedTiles::new(),
             globals: FrameGlobalResources::empty(),
             config: FrameBuilderConfig {
@@ -253,28 +256,36 @@ impl FrameBuilder {
     }
 
     pub fn with_display_list_flattener(
         output_rect: DeviceIntRect,
         background_color: Option<ColorF>,
         flattener: DisplayListFlattener,
     ) -> Self {
         FrameBuilder {
-            hit_testing_runs: flattener.hit_testing_runs,
+            hit_testing_scene: Arc::new(flattener.hit_testing_scene),
             prim_store: flattener.prim_store,
             clip_store: flattener.clip_store,
             root_pic_index: flattener.root_pic_index,
             output_rect,
             background_color,
             pending_retained_tiles: RetainedTiles::new(),
             config: flattener.config,
             globals: FrameGlobalResources::empty(),
         }
     }
 
+    /// Get the memory usage statistics to pre-allocate for the next scene.
+    pub fn get_stats(&self) -> DocumentStats {
+        DocumentStats {
+            prim_store_stats: self.prim_store.get_stats(),
+            hit_test_stats: self.hit_testing_scene.get_stats(),
+        }
+    }
+
     /// Destroy an existing frame builder. This is called just before
     /// a frame builder is replaced with a newly built scene.
     pub fn destroy(
         self,
         retained_tiles: &mut RetainedTiles,
         clip_scroll_tree: &ClipScrollTree,
     ) -> FrameGlobalResources {
         self.prim_store.destroy(
@@ -671,15 +682,15 @@ impl FrameBuilder {
     }
 
     pub fn create_hit_tester(
         &mut self,
         clip_scroll_tree: &ClipScrollTree,
         clip_data_store: &ClipDataStore,
     ) -> HitTester {
         HitTester::new(
-            &self.hit_testing_runs,
+            Arc::clone(&self.hit_testing_scene),
             clip_scroll_tree,
             &self.clip_store,
             clip_data_store,
         )
     }
 }
--- a/gfx/wr/webrender/src/hit_test.rs
+++ b/gfx/wr/webrender/src/hit_test.rs
@@ -1,35 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{BorderRadius, ClipMode, HitTestFlags, HitTestItem, HitTestResult, ItemTag, LayoutPoint};
 use api::{LayoutPrimitiveInfo, LayoutRect, PipelineId, WorldPoint};
-use clip::{ClipDataStore, ClipNode, ClipItem, ClipStore};
+use clip::{ClipChainId, ClipDataStore, ClipNode, ClipItem, ClipStore};
 use clip::{rounded_rectangle_contains_point};
 use clip_scroll_tree::{SpatialNodeIndex, ClipScrollTree};
 use internal_types::FastHashMap;
-use prim_store::ScrollNodeAndClipChain;
-use std::u32;
-use util::LayoutToWorldFastTransform;
+use std::{ops, u32};
+use std::sync::Arc;
+use util::{LayoutToWorldFastTransform, WorldToLayoutFastTransform};
 
 /// A copy of important clip scroll node data to use during hit testing. This a copy of
 /// data from the ClipScrollTree that will persist as a new frame is under construction,
 /// allowing hit tests consistent with the currently rendered frame.
 #[derive(MallocSizeOf)]
 pub struct HitTestSpatialNode {
     /// The pipeline id of this node.
     pipeline_id: PipelineId,
 
     /// World transform for content transformed by this node.
     world_content_transform: LayoutToWorldFastTransform,
 
     /// World viewport transform for content transformed by this node.
     world_viewport_transform: LayoutToWorldFastTransform,
+
+    /// Cached inverse of the world content transform
+    inv_world_content_transform: Option<WorldToLayoutFastTransform>,
+
+    /// Cached inverse of the world viewport transform
+    inv_world_viewport_transform: Option<WorldToLayoutFastTransform>,
 }
 
 #[derive(MallocSizeOf)]
 pub struct HitTestClipNode {
     /// A particular point must be inside all of these regions to be considered clipped in
     /// for the purposes of a hit test.
     region: HitTestRegion,
 }
@@ -53,58 +59,139 @@ impl HitTestClipNode {
         };
 
         HitTestClipNode {
             region,
         }
     }
 }
 
-// A hit testing clip chain node is the same as a
-// normal clip chain node, except that the clip
-// node is embedded inside the clip chain, rather
-// than referenced. This means we don't need to
-// copy the complete interned clip data store for
-// hit testing.
-
 #[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
 pub struct HitTestClipChainId(u32);
 
 impl HitTestClipChainId {
     pub const NONE: Self = HitTestClipChainId(u32::MAX);
 }
 
+/// A hit testing clip chain node is the same as a
+/// normal clip chain node, except that the clip
+/// node is embedded inside the clip chain, rather
+/// than referenced. This means we don't need to
+/// copy the complete interned clip data store for
+/// hit testing.
 #[derive(MallocSizeOf)]
 pub struct HitTestClipChainNode {
     pub region: HitTestClipNode,
     pub spatial_node_index: SpatialNodeIndex,
     pub parent_clip_chain_id: HitTestClipChainId,
 }
 
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+pub struct HitTestingClipChainIndex(u32);
+
 #[derive(Clone, MallocSizeOf)]
 pub struct HitTestingItem {
     rect: LayoutRect,
     clip_rect: LayoutRect,
     tag: ItemTag,
     is_backface_visible: bool,
+    #[ignore_malloc_size_of = "simple"]
+    clip_chain_range: ops::Range<HitTestingClipChainIndex>,
+    spatial_node_index: SpatialNodeIndex,
 }
 
 impl HitTestingItem {
-    pub fn new(tag: ItemTag, info: &LayoutPrimitiveInfo) -> HitTestingItem {
+    pub fn new(
+        tag: ItemTag,
+        info: &LayoutPrimitiveInfo,
+        spatial_node_index: SpatialNodeIndex,
+        clip_chain_range: ops::Range<HitTestingClipChainIndex>,
+    ) -> HitTestingItem {
         HitTestingItem {
             rect: info.rect,
             clip_rect: info.clip_rect,
             tag,
             is_backface_visible: info.is_backface_visible,
+            spatial_node_index,
+            clip_chain_range,
+        }
+    }
+}
+
+/// Statistics about allocation sizes of current hit tester,
+/// used to pre-allocate size of the next hit tester.
+pub struct HitTestingSceneStats {
+    pub clip_chain_roots_count: usize,
+    pub items_count: usize,
+}
+
+impl HitTestingSceneStats {
+    pub fn empty() -> Self {
+        HitTestingSceneStats {
+            clip_chain_roots_count: 0,
+            items_count: 0,
         }
     }
 }
 
-#[derive(Clone, MallocSizeOf)]
-pub struct HitTestingRun(pub Vec<HitTestingItem>, pub ScrollNodeAndClipChain);
+/// Defines the immutable part of a hit tester for a given scene.
+/// The hit tester is recreated each time a frame is built, since
+/// it relies on the current values of the clip scroll tree.
+/// However, the clip chain and item definitions don't change,
+/// so they are created once per scene, and shared between
+/// hit tester instances via Arc.
+#[derive(MallocSizeOf)]
+pub struct HitTestingScene {
+    /// The list of variable clip chain roots referenced by the items.
+    pub clip_chain_roots: Vec<HitTestClipChainId>,
+
+    /// List of hit testing primitives.
+    pub items: Vec<HitTestingItem>,
+}
+
+impl HitTestingScene {
+    /// Construct a new hit testing scene, pre-allocating to size
+    /// provided by previous scene stats.
+    pub fn new(stats: &HitTestingSceneStats) -> Self {
+        HitTestingScene {
+            clip_chain_roots: Vec::with_capacity(stats.clip_chain_roots_count),
+            items: Vec::with_capacity(stats.items_count),
+        }
+    }
+
+    /// Get stats about the current scene allocation sizes.
+    pub fn get_stats(&self) -> HitTestingSceneStats {
+        HitTestingSceneStats {
+            clip_chain_roots_count: self.clip_chain_roots.len(),
+            items_count: self.items.len(),
+        }
+    }
+
+    /// Add a hit testing primitive.
+    pub fn add_item(&mut self, item: HitTestingItem) {
+        self.items.push(item);
+    }
+
+    /// Add a clip chain to the clip chain roots list.
+    pub fn add_clip_chain(&mut self, clip_chain_id: ClipChainId) {
+        if clip_chain_id != ClipChainId::INVALID {
+            self.clip_chain_roots.push(HitTestClipChainId(clip_chain_id.0));
+        }
+    }
+
+    /// Get the slice of clip chain roots for a given hit test primitive.
+    fn get_clip_chains_for_item(&self, item: &HitTestingItem) -> &[HitTestClipChainId] {
+        &self.clip_chain_roots[item.clip_chain_range.start.0 as usize .. item.clip_chain_range.end.0 as usize]
+    }
+
+    /// Get the next index of the clip chain roots list.
+    pub fn next_clip_chain_index(&self) -> HitTestingClipChainIndex {
+        HitTestingClipChainIndex(self.clip_chain_roots.len() as u32)
+    }
+}
 
 #[derive(MallocSizeOf)]
 enum HitTestRegion {
     Invalid,
     Rectangle(LayoutRect, ClipMode),
     RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
 }
 
@@ -121,31 +208,32 @@ impl HitTestRegion {
                 !rounded_rectangle_contains_point(point, &rect, &radii),
             HitTestRegion::Invalid => true,
         }
     }
 }
 
 #[derive(MallocSizeOf)]
 pub struct HitTester {
-    runs: Vec<HitTestingRun>,
+    #[ignore_malloc_size_of = "Arc"]
+    scene: Arc<HitTestingScene>,
     spatial_nodes: Vec<HitTestSpatialNode>,
     clip_chains: Vec<HitTestClipChainNode>,
     pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
 }
 
 impl HitTester {
     pub fn new(
-        runs: &Vec<HitTestingRun>,
+        scene: Arc<HitTestingScene>,
         clip_scroll_tree: &ClipScrollTree,
         clip_store: &ClipStore,
         clip_data_store: &ClipDataStore,
     ) -> HitTester {
         let mut hit_tester = HitTester {
-            runs: runs.clone(),
+            scene,
             spatial_nodes: Vec::new(),
             clip_chains: Vec::new(),
             pipeline_root_nodes: FastHashMap::default(),
         };
         hit_tester.read_clip_scroll_tree(
             clip_scroll_tree,
             clip_store,
             clip_data_store,
@@ -168,16 +256,18 @@ impl HitTester {
 
             // If we haven't already seen a node for this pipeline, record this one as the root
             // node.
             self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
 
             self.spatial_nodes.push(HitTestSpatialNode {
                 pipeline_id: node.pipeline_id,
                 world_content_transform: node.world_content_transform,
+                inv_world_content_transform: node.world_content_transform.inverse(),
+                inv_world_viewport_transform: node.world_viewport_transform.inverse(),
                 world_viewport_transform: node.world_viewport_transform,
             });
         }
 
         // For each clip chain node, extract the clip node from the clip
         // data store, and store it inline with the clip chain node.
         self.clip_chains.reserve(clip_store.clip_chain_nodes.len());
         for node in &clip_store.clip_chain_nodes {
@@ -262,115 +352,146 @@ impl HitTester {
         }
 
         test.node_cache.insert(clip_chain_node_id, ClippedIn::ClippedIn);
         true
     }
 
     pub fn find_node_under_point(&self, mut test: HitTest) -> Option<SpatialNodeIndex> {
         let point = test.get_absolute_point(self);
+        let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+        let mut point_in_layer = None;
 
-        for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
-            let spatial_node_index = clip_and_scroll.spatial_node_index;
-            let scroll_node = &self.spatial_nodes[spatial_node_index.0 as usize];
-            let transform = scroll_node.world_content_transform;
-            let point_in_layer = match transform
-                .inverse()
-                .and_then(|inverted| inverted.transform_point2d(&point))
-            {
-                Some(point) => point,
-                None => continue,
-            };
+        // For each hit test primitive
+        for item in self.scene.items.iter().rev() {
+            let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
+
+            // Update the cached point in layer space, if the spatial node
+            // changed since last primitive.
+            if item.spatial_node_index != current_spatial_node_index {
+                point_in_layer = scroll_node
+                    .inv_world_content_transform
+                    .and_then(|inverted| inverted.transform_point2d(&point));
 
-            let mut clipped_in = false;
-            for item in items.iter().rev() {
-                if !item.rect.contains(&point_in_layer) ||
-                    !item.clip_rect.contains(&point_in_layer) {
+                current_spatial_node_index = item.spatial_node_index;
+            }
+
+            // Only consider hit tests on transformable layers.
+            if let Some(point_in_layer) = point_in_layer {
+                // If the item's rect or clip rect don't contain this point,
+                // it's not a valid hit.
+                if !item.rect.contains(&point_in_layer) {
+                    continue;
+                }
+                if !item.clip_rect.contains(&point_in_layer) {
                     continue;
                 }
 
-                let clip_chain_id = HitTestClipChainId(clip_and_scroll.clip_chain_id.0);
-                clipped_in |=
-                    self.is_point_clipped_in_for_clip_chain(point, clip_chain_id, &mut test);
-                if !clipped_in {
-                    break;
+                // See if any of the clip chain roots for this primitive
+                // cull out the item.
+                let clip_chains = self.scene.get_clip_chains_for_item(item);
+                let mut is_valid = true;
+                for clip_chain_id in clip_chains {
+                    if !self.is_point_clipped_in_for_clip_chain(point, *clip_chain_id, &mut test) {
+                        is_valid = false;
+                        break;
+                    }
                 }
 
-                return Some(spatial_node_index);
+                // Found a valid hit test result!
+                if is_valid {
+                    return Some(item.spatial_node_index);
+                }
             }
         }
 
         None
     }
 
     pub fn hit_test(&self, mut test: HitTest) -> HitTestResult {
         let point = test.get_absolute_point(self);
 
         let mut result = HitTestResult::default();
-        for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
-            let spatial_node_index = clip_and_scroll.spatial_node_index;
-            let scroll_node = &self.spatial_nodes[spatial_node_index.0 as usize];
+        let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+        let mut point_in_layer = None;
+        let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
+        let mut point_in_viewport = None;
+
+        // For each hit test primitive
+        for item in self.scene.items.iter().rev() {
+            let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
             let pipeline_id = scroll_node.pipeline_id;
             match (test.pipeline_id, pipeline_id) {
                 (Some(id), node_id) if node_id != id => continue,
                 _ => {},
             }
 
-            let transform = scroll_node.world_content_transform;
-            let mut facing_backwards: Option<bool> = None;  // will be computed on first use
-            let point_in_layer = match transform
-                .inverse()
-                .and_then(|inverted| inverted.transform_point2d(&point))
-            {
-                Some(point) => point,
-                None => continue,
-            };
+            // Update the cached point in layer space, if the spatial node
+            // changed since last primitive.
+            if item.spatial_node_index != current_spatial_node_index {
+                point_in_layer = scroll_node
+                    .inv_world_content_transform
+                    .and_then(|inverted| inverted.transform_point2d(&point));
+                current_spatial_node_index = item.spatial_node_index;
+            }
 
-            let mut clipped_in = false;
-            for item in items.iter().rev() {
-                if !item.rect.contains(&point_in_layer) ||
-                    !item.clip_rect.contains(&point_in_layer) {
+            // Only consider hit tests on transformable layers.
+            if let Some(point_in_layer) = point_in_layer {
+                // If the item's rect or clip rect don't contain this point,
+                // it's not a valid hit.
+                if !item.rect.contains(&point_in_layer) {
+                    continue;
+                }
+                if !item.clip_rect.contains(&point_in_layer) {
                     continue;
                 }
 
-                let clip_chain_id = HitTestClipChainId(clip_and_scroll.clip_chain_id.0);
-                clipped_in = clipped_in ||
-                    self.is_point_clipped_in_for_clip_chain(point, clip_chain_id, &mut test);
-                if !clipped_in {
-                    break;
+                // See if any of the clip chain roots for this primitive
+                // cull out the item.
+                let clip_chains = self.scene.get_clip_chains_for_item(item);
+                let mut is_valid = true;
+                for clip_chain_id in clip_chains {
+                    if !self.is_point_clipped_in_for_clip_chain(point, *clip_chain_id, &mut test) {
+                        is_valid = false;
+                        break;
+                    }
+                }
+                if !is_valid {
+                    continue;
                 }
 
                 // Don't hit items with backface-visibility:hidden if they are facing the back.
-                if !item.is_backface_visible {
-                    if *facing_backwards.get_or_insert_with(|| transform.is_backface_visible()) {
-                        continue;
-                    }
+                if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
+                    continue;
                 }
 
                 // We need to calculate the position of the test point relative to the origin of
                 // the pipeline of the hit item. If we cannot get a transformed point, we are
                 // in a situation with an uninvertible transformation so we should just skip this
                 // result.
-                let root_node = &self.spatial_nodes[self.pipeline_root_nodes[&pipeline_id].0 as usize];
-                let point_in_viewport = match root_node.world_viewport_transform
-                    .inverse()
-                    .and_then(|inverted| inverted.transform_point2d(&point))
-                {
-                    Some(point) => point,
-                    None => continue,
-                };
+                let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
+                if root_spatial_node_index != current_root_spatial_node_index {
+                    let root_node = &self.spatial_nodes[root_spatial_node_index.0 as usize];
+                    point_in_viewport = root_node
+                        .inv_world_viewport_transform
+                        .and_then(|inverted| inverted.transform_point2d(&point));
+                    current_root_spatial_node_index = root_spatial_node_index;
+                }
 
-                result.items.push(HitTestItem {
-                    pipeline: pipeline_id,
-                    tag: item.tag,
-                    point_in_viewport,
-                    point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
-                });
-                if !test.flags.contains(HitTestFlags::FIND_ALL) {
-                    return result;
+                if let Some(point_in_viewport) = point_in_viewport {
+                    result.items.push(HitTestItem {
+                        pipeline: pipeline_id,
+                        tag: item.tag,
+                        point_in_viewport,
+                        point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
+                    });
+
+                    if !test.flags.contains(HitTestFlags::FIND_ALL) {
+                        return result;
+                    }
                 }
             }
         }
 
         result.items.dedup();
         result
     }
 
--- a/gfx/wr/webrender/src/scene_builder.rs
+++ b/gfx/wr/webrender/src/scene_builder.rs
@@ -7,16 +7,17 @@ use api::{DocumentId, PipelineId, ApiMsg
 use api::{BuiltDisplayList, ColorF, LayoutSize, NotificationRequest, Checkpoint, IdNamespace};
 use api::{ClipIntern, FilterDataIntern, MemoryReport, PrimitiveKeyKind};
 use api::channel::MsgSender;
 #[cfg(feature = "capture")]
 use capture::CaptureConfig;
 use frame_builder::{FrameBuilderConfig, FrameBuilder};
 use clip_scroll_tree::ClipScrollTree;
 use display_list_flattener::DisplayListFlattener;
+use hit_test::HitTestingSceneStats;
 use intern::{Internable, Interner, UpdateList};
 use internal_types::{FastHashMap, FastHashSet};
 use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
 use prim_store::PrimitiveStoreStats;
 use prim_store::borders::{ImageBorder, NormalBorderPrim};
 use prim_store::gradient::{LinearGradient, RadialGradient};
 use prim_store::image::{Image, YuvImage};
 use prim_store::line_dec::LineDecoration;
@@ -219,32 +220,50 @@ macro_rules! declare_interners {
                 }
             }
         }
     }
 }
 
 enumerate_interners!(declare_interners);
 
+/// Stores the allocation sizes of various arrays in the frame
+/// builder. This is retrieved from the current frame builder
+/// and used to reserve an approximately correct capacity of
+/// the arrays for the next scene that is getting built.
+pub struct DocumentStats {
+    pub prim_store_stats: PrimitiveStoreStats,
+    pub hit_test_stats: HitTestingSceneStats,
+}
+
+impl DocumentStats {
+    pub fn empty() -> DocumentStats {
+        DocumentStats {
+            prim_store_stats: PrimitiveStoreStats::empty(),
+            hit_test_stats: HitTestingSceneStats::empty(),
+        }
+    }
+}
+
 // A document in the scene builder contains the current scene,
 // as well as a persistent clip interner. This allows clips
 // to be de-duplicated, and persisted in the GPU cache between
 // display lists.
 struct Document {
     scene: Scene,
     interners: Interners,
-    prim_store_stats: PrimitiveStoreStats,
+    doc_stats: DocumentStats,
 }
 
 impl Document {
     fn new(scene: Scene) -> Self {
         Document {
             scene,
             interners: Interners::default(),
-            prim_store_stats: PrimitiveStoreStats::empty(),
+            doc_stats: DocumentStats::empty(),
         }
     }
 }
 
 pub struct SceneBuilder {
     documents: FastHashMap<DocumentId, Document>,
     rx: Receiver<SceneBuilderRequest>,
     tx: Sender<SceneBuilderResult>,
@@ -381,17 +400,17 @@ impl SceneBuilder {
                     &item.scene,
                     &mut clip_scroll_tree,
                     item.font_instances,
                     &item.view,
                     &item.output_pipelines,
                     &self.config,
                     &mut new_scene,
                     &mut item.interners,
-                    &PrimitiveStoreStats::empty(),
+                    &DocumentStats::empty(),
                 );
 
                 interner_updates = Some(
                     item.interners.end_frame_and_get_pending_updates()
                 );
 
                 built_scene = Some(BuiltScene {
                     scene: new_scene,
@@ -400,17 +419,17 @@ impl SceneBuilder {
                 });
             }
 
             self.documents.insert(
                 item.document_id,
                 Document {
                     scene: item.scene,
                     interners: item.interners,
-                    prim_store_stats: PrimitiveStoreStats::empty(),
+                    doc_stats: DocumentStats::empty(),
                 },
             );
 
             let txn = Box::new(BuiltTransaction {
                 document_id: item.document_id,
                 render_frame: item.build_frame,
                 invalidate_rendered_frame: false,
                 built_scene,
@@ -476,21 +495,21 @@ impl SceneBuilder {
                     &scene,
                     &mut clip_scroll_tree,
                     request.font_instances,
                     &request.view,
                     &request.output_pipelines,
                     &self.config,
                     &mut new_scene,
                     &mut doc.interners,
-                    &doc.prim_store_stats,
+                    &doc.doc_stats,
                 );
 
                 // Update the allocation stats for next scene
-                doc.prim_store_stats = frame_builder.prim_store.get_stats();
+                doc.doc_stats = frame_builder.get_stats();
 
                 // Retrieve the list of updates from the clip interner.
                 interner_updates = Some(
                     doc.interners.end_frame_and_get_pending_updates()
                 );
 
                 built_scene = Some(BuiltScene {
                     scene: new_scene,
--- a/gfx/wr/webrender/src/util.rs
+++ b/gfx/wr/webrender/src/util.rs
@@ -804,16 +804,17 @@ impl<Src, Dst> From<TypedTransform3D<f32
 impl<Src, Dst> From<TypedVector2D<f32, Src>> for FastTransform<Src, Dst> {
     fn from(vector: TypedVector2D<f32, Src>) -> Self {
         FastTransform::with_vector(vector)
     }
 }
 
 pub type LayoutFastTransform = FastTransform<LayoutPixel, LayoutPixel>;
 pub type LayoutToWorldFastTransform = FastTransform<LayoutPixel, WorldPixel>;
+pub type WorldToLayoutFastTransform = FastTransform<WorldPixel, LayoutPixel>;
 
 pub fn project_rect<F, T>(
     transform: &TypedTransform3D<f32, F, T>,
     rect: &TypedRect<f32, F>,
     bounds: &TypedRect<f32, T>,
 ) -> Option<TypedRect<f32, T>>
  where F: fmt::Debug
 {